@burdenoff/microfe-subscriberbot 2026.801.1 → 2026.829.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/subscriberbot/admin/AdminDashboardPage.d.ts.map +1 -1
  2. package/dist/subscriberbot/admin/AdminDashboardPage.js +80 -83
  3. package/dist/subscriberbot/admin/AdminDashboardPage.js.map +1 -1
  4. package/dist/subscriberbot/components/viz.js +51 -27
  5. package/dist/subscriberbot/components/viz.js.map +1 -1
  6. package/dist/subscriberbot/hooks/index.d.ts +2 -1
  7. package/dist/subscriberbot/hooks/index.d.ts.map +1 -1
  8. package/dist/subscriberbot/hooks/index.js +1 -0
  9. package/dist/subscriberbot/hooks/useSubscriberBotApi.d.ts +12 -1
  10. package/dist/subscriberbot/hooks/useSubscriberBotApi.d.ts.map +1 -1
  11. package/dist/subscriberbot/hooks/useSubscriberBotApi.js +82 -56
  12. package/dist/subscriberbot/hooks/useSubscriberBotApi.js.map +1 -1
  13. package/dist/subscriberbot/hooks/useTr.d.ts +17 -0
  14. package/dist/subscriberbot/hooks/useTr.d.ts.map +1 -0
  15. package/dist/subscriberbot/hooks/useTr.js +14 -0
  16. package/dist/subscriberbot/hooks/useTr.js.map +1 -0
  17. package/dist/subscriberbot/pages/AssetVaultPage.d.ts.map +1 -1
  18. package/dist/subscriberbot/pages/AssetVaultPage.js +155 -97
  19. package/dist/subscriberbot/pages/AssetVaultPage.js.map +1 -1
  20. package/dist/subscriberbot/pages/DashboardPage.d.ts.map +1 -1
  21. package/dist/subscriberbot/pages/DashboardPage.js +113 -105
  22. package/dist/subscriberbot/pages/DashboardPage.js.map +1 -1
  23. package/dist/subscriberbot/pages/InboxPage.d.ts.map +1 -1
  24. package/dist/subscriberbot/pages/InboxPage.js +404 -369
  25. package/dist/subscriberbot/pages/InboxPage.js.map +1 -1
  26. package/dist/subscriberbot/pages/InsightsPage.d.ts.map +1 -1
  27. package/dist/subscriberbot/pages/InsightsPage.js +94 -93
  28. package/dist/subscriberbot/pages/InsightsPage.js.map +1 -1
  29. package/dist/subscriberbot/pages/InvoicesPage.d.ts +3 -2
  30. package/dist/subscriberbot/pages/InvoicesPage.d.ts.map +1 -1
  31. package/dist/subscriberbot/pages/InvoicesPage.js +368 -362
  32. package/dist/subscriberbot/pages/InvoicesPage.js.map +1 -1
  33. package/dist/subscriberbot/pages/WorkflowsPage.d.ts.map +1 -1
  34. package/dist/subscriberbot/pages/WorkflowsPage.js +209 -193
  35. package/dist/subscriberbot/pages/WorkflowsPage.js.map +1 -1
  36. package/dist/subscriberbot/pages/agents/AgentDetailPage.d.ts +3 -1
  37. package/dist/subscriberbot/pages/agents/AgentDetailPage.d.ts.map +1 -1
  38. package/dist/subscriberbot/pages/agents/AgentDetailPage.js +296 -228
  39. package/dist/subscriberbot/pages/agents/AgentDetailPage.js.map +1 -1
  40. package/dist/subscriberbot/pages/agents/AgentNewPage.d.ts.map +1 -1
  41. package/dist/subscriberbot/pages/agents/AgentNewPage.js +96 -76
  42. package/dist/subscriberbot/pages/agents/AgentNewPage.js.map +1 -1
  43. package/dist/subscriberbot/pages/agents/AgentsListPage.d.ts.map +1 -1
  44. package/dist/subscriberbot/pages/agents/AgentsListPage.js +145 -161
  45. package/dist/subscriberbot/pages/agents/AgentsListPage.js.map +1 -1
  46. package/dist/subscriberbot/pages/providers/ProviderDetailPage.d.ts +4 -2
  47. package/dist/subscriberbot/pages/providers/ProviderDetailPage.d.ts.map +1 -1
  48. package/dist/subscriberbot/pages/providers/ProviderDetailPage.js +577 -402
  49. package/dist/subscriberbot/pages/providers/ProviderDetailPage.js.map +1 -1
  50. package/dist/subscriberbot/pages/providers/ProviderNewPage.d.ts.map +1 -1
  51. package/dist/subscriberbot/pages/providers/ProviderNewPage.js +167 -129
  52. package/dist/subscriberbot/pages/providers/ProviderNewPage.js.map +1 -1
  53. package/dist/subscriberbot/pages/providers/ProvidersListPage.d.ts.map +1 -1
  54. package/dist/subscriberbot/pages/providers/ProvidersListPage.js +171 -218
  55. package/dist/subscriberbot/pages/providers/ProvidersListPage.js.map +1 -1
  56. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.d.ts.map +1 -1
  57. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js +340 -279
  58. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js.map +1 -1
  59. package/dist/subscriberbot/pages/relationships/RelationshipNewPage.d.ts.map +1 -1
  60. package/dist/subscriberbot/pages/relationships/RelationshipNewPage.js +185 -166
  61. package/dist/subscriberbot/pages/relationships/RelationshipNewPage.js.map +1 -1
  62. package/dist/subscriberbot/pages/relationships/RelationshipsListPage.d.ts.map +1 -1
  63. package/dist/subscriberbot/pages/relationships/RelationshipsListPage.js +153 -182
  64. package/dist/subscriberbot/pages/relationships/RelationshipsListPage.js.map +1 -1
  65. package/dist/subscriberbot/types/index.d.ts +37 -0
  66. package/dist/subscriberbot/types/index.d.ts.map +1 -1
  67. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"InboxPage.js","names":[],"sources":["../../../src/subscriberbot/pages/InboxPage.tsx"],"sourcesContent":["/**\n * Universal Inbox\n * @module subscriberbot/pages\n *\n * Every merged subscription communication in one timeline, AI-categorized,\n * plus management of the ingest channels that feed it. Filter by category /\n * unread, mark messages read, add & verify channels, and log a test message.\n * Reads `subListInboxMessages` / `subListChannels`; writes `subMarkMessageRead`,\n * `subCreateChannel`, `subVerifyChannel`, `subIngestMessage`.\n */\n\nimport { useState } from 'react';\nimport {\n Mail,\n MailOpen,\n AlertTriangle,\n Inbox as InboxIcon,\n Plus,\n Radio,\n BadgeCheck,\n Send,\n MessageSquare,\n MessagesSquare,\n Bell,\n Webhook,\n Phone,\n Hash,\n ShieldAlert,\n Sparkles,\n ChevronRight,\n MousePointerClick,\n type LucideIcon,\n} from 'lucide-react';\nimport { PagePurpose, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { StatusBadge } from '../components/StatusBadge';\nimport { EmptyState } from '../components/EmptyState';\nimport { Modal } from '../components/Modal';\nimport { LoadingView, ErrorView, Section } from '../components/StateViews';\nimport { MiniProgress } from '../components/viz';\nimport { useSubscriberBot } from '../context/SubscriberBotContext';\nimport {\n useInboxMessages,\n useMarkMessageRead,\n useChannels,\n useCreateChannel,\n useVerifyChannel,\n useIngestMessage,\n} from '../hooks/useSubscriberBotApi';\nimport { cn } from '../utils/cn';\nimport { formatEnumLabel, formatRelativeTime, formatDateTime } from '../utils/formatters';\nimport type {\n SubChannelType,\n SubCommunicationChannel,\n SubInboxMessage,\n SubMessageCategory,\n} from '../types';\n\nconst PAGE_SIZE = 25;\n\nconst inputCls =\n 'w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\nconst labelCls = 'mb-1 block text-xs font-medium text-text-secondary';\n\n// Category filter tabs (chips) — the \"All\" pseudo-value plus the real enum.\nconst CATEGORY_TABS: Array<{ value: '' | SubMessageCategory; label: string }> = [\n { value: '', label: 'All' },\n { value: 'BILLING', label: 'Billing' },\n { value: 'RENEWAL', label: 'Renewals' },\n { value: 'REQUIRED_ACTION', label: 'Required actions' },\n { value: 'SECURITY_ALERT', label: 'Security' },\n { value: 'PRODUCT_UPDATE', label: 'Product updates' },\n { value: 'MARKETING', label: 'Marketing' },\n { value: 'OTHER', label: 'Other' },\n];\n\nconst MESSAGE_CATEGORIES: SubMessageCategory[] = [\n 'BILLING',\n 'RENEWAL',\n 'REQUIRED_ACTION',\n 'SECURITY_ALERT',\n 'PRODUCT_UPDATE',\n 'MARKETING',\n 'OTHER',\n];\n\nconst CHANNEL_TYPES: SubChannelType[] = [\n 'EMAIL',\n 'SMS',\n 'SLACK',\n 'DISCORD',\n 'WHATSAPP',\n 'PUSH',\n 'WEBHOOK',\n];\n\n// Per-category tag tone — semantic tokens only.\nconst CATEGORY_TONE: Record<SubMessageCategory, string> = {\n BILLING: 'bg-[var(--color-accent-soft)] text-text-primary',\n RENEWAL: 'bg-status-success-bg-subtle text-status-success-text',\n REQUIRED_ACTION: 'bg-status-warning-bg-subtle text-status-warning-text',\n SECURITY_ALERT: 'bg-status-error-bg-subtle text-status-error-text',\n PRODUCT_UPDATE: 'bg-[var(--color-accent-soft)] text-text-primary',\n MARKETING: 'bg-bg-sunken text-text-secondary',\n OTHER: 'bg-bg-sunken text-text-secondary',\n};\n\nconst CHANNEL_ICON: Record<SubChannelType, LucideIcon> = {\n EMAIL: Mail,\n SMS: MessageSquare,\n SLACK: Hash,\n DISCORD: MessagesSquare,\n WHATSAPP: Phone,\n PUSH: Bell,\n WEBHOOK: Webhook,\n};\n\nfunction CategoryTag({ category }: { category: SubMessageCategory }) {\n return (\n <span\n className={cn(\n 'inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium',\n CATEGORY_TONE[category]\n )}\n >\n {formatEnumLabel(category)}\n </span>\n );\n}\n\nfunction ActionFlag({ className }: { className?: string }) {\n return (\n <span\n title=\"Action required\"\n className={cn(\n 'inline-flex items-center gap-1 rounded-full bg-status-warning-bg-subtle px-2 py-0.5 text-[10px] font-medium text-status-warning-text',\n className\n )}\n >\n <AlertTriangle className=\"h-3 w-3\" /> Action\n </span>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Add channel modal → subCreateChannel\n// ---------------------------------------------------------------------------\n\nfunction AddChannelModal({ onClose }: { onClose: () => void }) {\n const create = useCreateChannel();\n const [type, setType] = useState<SubChannelType>('EMAIL');\n const [address, setAddress] = useState('');\n const [label, setLabel] = useState('');\n const [ingestEnabled, setIngestEnabled] = useState(true);\n const [formError, setFormError] = useState<string | null>(null);\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n setFormError(null);\n if (!address.trim()) {\n setFormError('An address is required.');\n return;\n }\n create.mutate(\n {\n type,\n address: address.trim(),\n ingestEnabled,\n ...(label.trim() ? { label: label.trim() } : {}),\n },\n {\n onSuccess: () => onClose(),\n onError: (err) =>\n setFormError(err instanceof Error ? err.message : 'Could not add channel.'),\n }\n );\n };\n\n return (\n <Modal title=\"Add channel\" onClose={onClose} maxWidthClassName=\"max-w-xl\">\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {formError && (\n <div className=\"rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {formError}\n </div>\n )}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"ch-type\" className={labelCls}>\n Type\n </label>\n <select\n id=\"ch-type\"\n value={type}\n onChange={(e) => setType(e.target.value as SubChannelType)}\n className={inputCls}\n >\n {CHANNEL_TYPES.map((t) => (\n <option key={t} value={t}>\n {t.replace(/_/g, ' ')}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"ch-label\" className={labelCls}>\n Label\n </label>\n <input\n id=\"ch-label\"\n type=\"text\"\n value={label}\n onChange={(e) => setLabel(e.target.value)}\n placeholder=\"e.g. Billing inbox\"\n className={inputCls}\n />\n </div>\n </div>\n <div>\n <label htmlFor=\"ch-address\" className={labelCls}>\n Address <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"ch-address\"\n type=\"text\"\n value={address}\n onChange={(e) => setAddress(e.target.value)}\n placeholder=\"e.g. billing@acme.com, +1…, a webhook URL\"\n className={inputCls}\n required\n />\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={ingestEnabled}\n onChange={(e) => setIngestEnabled(e.target.checked)}\n className=\"size-4 rounded border-border-subtle\"\n />\n Ingest messages from this channel\n </label>\n <div className=\"flex justify-end gap-3\">\n <button\n type=\"button\"\n onClick={onClose}\n className=\"rounded-lg border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={create.isPending}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n {create.isPending ? 'Adding…' : 'Add channel'}\n </button>\n </div>\n </form>\n </Modal>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Log test message modal → subIngestMessage\n// ---------------------------------------------------------------------------\n\nfunction LogMessageModal({\n channel,\n onClose,\n}: {\n channel: SubCommunicationChannel;\n onClose: () => void;\n}) {\n const ingest = useIngestMessage();\n const [subject, setSubject] = useState('');\n const [preview, setPreview] = useState('');\n const [category, setCategory] = useState<SubMessageCategory>('OTHER');\n const [actionRequired, setActionRequired] = useState(false);\n const [formError, setFormError] = useState<string | null>(null);\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n setFormError(null);\n ingest.mutate(\n {\n channelId: channel.id,\n receivedAt: new Date().toISOString(),\n category,\n actionRequired,\n ...(subject.trim() ? { subject: subject.trim() } : {}),\n ...(preview.trim() ? { preview: preview.trim() } : {}),\n },\n {\n onSuccess: () => onClose(),\n onError: (err) =>\n setFormError(err instanceof Error ? err.message : 'Could not log the message.'),\n }\n );\n };\n\n return (\n <Modal\n title=\"Log test message\"\n description={`Simulate an ingested message from ${channel.label || channel.address}.`}\n onClose={onClose}\n >\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {formError && (\n <div className=\"rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {formError}\n </div>\n )}\n <div>\n <label htmlFor=\"msg-subject\" className={labelCls}>\n Subject\n </label>\n <input\n id=\"msg-subject\"\n type=\"text\"\n value={subject}\n onChange={(e) => setSubject(e.target.value)}\n placeholder=\"e.g. Your plan renews in 3 days\"\n className={inputCls}\n />\n </div>\n <div>\n <label htmlFor=\"msg-preview\" className={labelCls}>\n Preview\n </label>\n <textarea\n id=\"msg-preview\"\n value={preview}\n onChange={(e) => setPreview(e.target.value)}\n rows={2}\n className={inputCls}\n />\n </div>\n <div>\n <label htmlFor=\"msg-category\" className={labelCls}>\n Category\n </label>\n <select\n id=\"msg-category\"\n value={category}\n onChange={(e) => setCategory(e.target.value as SubMessageCategory)}\n className={inputCls}\n >\n {MESSAGE_CATEGORIES.map((c) => (\n <option key={c} value={c}>\n {c.replace(/_/g, ' ')}\n </option>\n ))}\n </select>\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={actionRequired}\n onChange={(e) => setActionRequired(e.target.checked)}\n className=\"size-4 rounded border-border-subtle\"\n />\n Requires action\n </label>\n <div className=\"flex justify-end gap-3\">\n <button\n type=\"button\"\n onClick={onClose}\n className=\"rounded-lg border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={ingest.isPending}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n {ingest.isPending ? 'Logging…' : 'Log message'}\n </button>\n </div>\n </form>\n </Modal>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Channel card → one per ingest source\n// ---------------------------------------------------------------------------\n\nfunction ChannelCard({\n channel,\n onVerify,\n verifying,\n onLog,\n}: {\n channel: SubCommunicationChannel;\n onVerify: (id: string) => void;\n verifying: boolean;\n onLog: (ch: SubCommunicationChannel) => void;\n}) {\n const Icon = CHANNEL_ICON[channel.type] ?? Radio;\n const verified = channel.status === 'VERIFIED';\n return (\n <GlassCard treatment=\"metric\" className=\"flex flex-col p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-2.5\">\n <span className=\"flex size-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <Icon className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {formatEnumLabel(channel.type)}\n </p>\n {channel.label && (\n <p className=\"truncate text-xs text-text-secondary\">{channel.label}</p>\n )}\n </div>\n </div>\n <StatusBadge status={channel.status} />\n </div>\n\n <div className=\"mt-3 border-t border-border-subtle pt-3\">\n <p className=\"text-[11px] uppercase tracking-wide text-text-secondary\">\n {channel.ingestEnabled ? 'Ingesting from' : 'Ingest paused'}\n </p>\n <p className=\"mt-0.5 truncate text-sm text-text-primary\" title={channel.address}>\n {channel.address}\n </p>\n </div>\n\n <div className=\"mt-auto flex flex-wrap items-center gap-2 pt-3\">\n {channel.status === 'PENDING' && (\n <button\n type=\"button\"\n disabled={verifying}\n onClick={() => onVerify(channel.id)}\n className=\"inline-flex items-center gap-1 rounded-lg bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n <BadgeCheck className=\"h-3.5 w-3.5\" /> Verify\n </button>\n )}\n {verified && channel.ingestEnabled && (\n <button\n type=\"button\"\n onClick={() => onLog(channel)}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <Send className=\"h-3.5 w-3.5\" /> Log test message\n </button>\n )}\n {channel.status === 'DISABLED' && (\n <span className=\"text-xs text-text-secondary\">Channel disabled</span>\n )}\n </div>\n </GlassCard>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Channels management panel\n// ---------------------------------------------------------------------------\n\nfunction ChannelsSection() {\n const { data, isLoading, isError, refetch } = useChannels();\n const verify = useVerifyChannel();\n const [showAdd, setShowAdd] = useState(false);\n const [logChannel, setLogChannel] = useState<SubCommunicationChannel | null>(null);\n\n const channels = data ?? [];\n const verifiedCount = channels.filter((c) => c.status === 'VERIFIED').length;\n const pendingCount = channels.filter((c) => c.status === 'PENDING').length;\n\n return (\n <Section\n title=\"Ingest channels\"\n description=\"The sources the Universal Inbox pulls subscription communication from\"\n action={\n <button\n type=\"button\"\n onClick={() => setShowAdd(true)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:opacity-90\"\n >\n <Plus className=\"h-3.5 w-3.5\" /> Add channel\n </button>\n }\n >\n {isLoading ? (\n <LoadingView label=\"Loading channels…\" />\n ) : isError ? (\n <ErrorView message=\"Could not load channels.\" onRetry={() => void refetch()} />\n ) : channels.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Radio className=\"h-12 w-12\" />}\n title=\"No channels yet\"\n description=\"Add an email, SMS, Slack or webhook channel to start ingesting subscription communication.\"\n />\n ) : (\n <>\n {(verifiedCount > 0 || pendingCount > 0) && (\n <div className=\"mb-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"size-1.5 rounded-full bg-status-success-text\" aria-hidden />\n {verifiedCount} verified\n </span>\n {pendingCount > 0 && (\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"size-1.5 rounded-full bg-status-warning-text\" aria-hidden />\n {pendingCount} pending verification\n </span>\n )}\n </div>\n )}\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {channels.map((ch) => (\n <ChannelCard\n key={ch.id}\n channel={ch}\n verifying={verify.isPending}\n onVerify={(id) => verify.mutate(id)}\n onLog={(c) => setLogChannel(c)}\n />\n ))}\n </div>\n </>\n )}\n\n {showAdd && <AddChannelModal onClose={() => setShowAdd(false)} />}\n {logChannel && <LogMessageModal channel={logChannel} onClose={() => setLogChannel(null)} />}\n </Section>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Message row (list)\n// ---------------------------------------------------------------------------\n\nfunction MessageRow({\n message,\n active,\n onSelect,\n onMarkRead,\n markPending,\n onViewRelationship,\n}: {\n message: SubInboxMessage;\n active: boolean;\n onSelect: () => void;\n onMarkRead: () => void;\n markPending: boolean;\n onViewRelationship: (() => void) | null;\n}) {\n const unread = !message.isRead;\n return (\n <div\n role=\"button\"\n tabIndex={0}\n aria-pressed={active}\n onClick={onSelect}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onSelect();\n }\n }}\n className={cn(\n 'relative flex cursor-pointer items-start gap-3 p-4 transition-colors',\n 'focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] focus-visible:ring-inset',\n active ? 'bg-bg-sunken' : unread ? 'bg-accent-wash' : 'hover:bg-bg-sunken/60'\n )}\n >\n {/* unread accent rail */}\n {unread && <span aria-hidden className=\"absolute inset-y-0 left-0 w-0.5 bg-text-link\" />}\n <span className=\"mt-0.5 shrink-0 text-text-secondary\">\n {unread ? <Mail className=\"h-4 w-4 text-text-link\" /> : <MailOpen className=\"h-4 w-4\" />}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2\">\n {unread && (\n <span aria-label=\"Unread\" className=\"size-1.5 shrink-0 rounded-full bg-text-link\" />\n )}\n <p\n className={cn(\n 'min-w-0 flex-1 truncate text-sm',\n unread ? 'font-semibold text-text-primary' : 'font-normal text-text-primary'\n )}\n >\n {message.subject || '(no subject)'}\n </p>\n <span className=\"shrink-0 text-xs tabular-nums text-text-secondary\">\n {formatRelativeTime(message.receivedAt)}\n </span>\n </div>\n {message.preview && (\n <p className=\"mt-0.5 line-clamp-2 text-xs text-text-secondary\">{message.preview}</p>\n )}\n <div className=\"mt-1.5 flex flex-wrap items-center gap-2\">\n <CategoryTag category={message.category} />\n {message.actionRequired && <ActionFlag />}\n {onViewRelationship && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onViewRelationship();\n }}\n className=\"text-xs font-medium text-text-link hover:underline\"\n >\n View relationship\n </button>\n )}\n {unread && (\n <button\n type=\"button\"\n disabled={markPending}\n onClick={(e) => {\n e.stopPropagation();\n onMarkRead();\n }}\n className=\"ml-auto shrink-0 rounded-lg border border-border-subtle px-2.5 py-1 text-xs font-medium text-text-primary hover:bg-bg-sunken disabled:opacity-50\"\n >\n Mark read\n </button>\n )}\n </div>\n </div>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Reading pane (wide screens)\n// ---------------------------------------------------------------------------\n\nfunction ReadingPane({\n message,\n channel,\n onMarkRead,\n markPending,\n onViewRelationship,\n}: {\n message: SubInboxMessage | null;\n channel: SubCommunicationChannel | undefined;\n onMarkRead: () => void;\n markPending: boolean;\n onViewRelationship: (() => void) | null;\n}) {\n if (!message) {\n return (\n <GlassCard\n treatment=\"metric\"\n className=\"flex min-h-[20rem] flex-col items-center justify-center p-8 text-center\"\n >\n <MousePointerClick className=\"h-10 w-10 text-text-secondary\" />\n <p className=\"mt-3 text-sm font-medium text-text-primary\">Select a message</p>\n <p className=\"mt-1 max-w-[16rem] text-xs text-text-secondary\">\n Pick any message from the list to read the full details here.\n </p>\n </GlassCard>\n );\n }\n\n const Icon = channel ? (CHANNEL_ICON[channel.type] ?? Radio) : Radio;\n const confidencePct =\n message.categoryConfidence != null\n ? Math.round(Math.max(0, Math.min(1, message.categoryConfidence)) * 100)\n : null;\n\n return (\n <GlassCard treatment=\"metric\" className=\"flex flex-col p-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <h3 className=\"text-base font-semibold text-text-primary\">\n {message.subject || '(no subject)'}\n </h3>\n {!message.isRead && (\n <button\n type=\"button\"\n disabled={markPending}\n onClick={onMarkRead}\n className=\"shrink-0 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-primary hover:bg-bg-sunken disabled:opacity-50\"\n >\n Mark read\n </button>\n )}\n </div>\n\n {channel && (\n <div className=\"mt-2 flex items-center gap-2 text-sm\">\n <Icon className=\"h-4 w-4 shrink-0 text-text-secondary\" />\n <span className=\"truncate text-text-secondary\" title={channel.address}>\n {channel.address}\n </span>\n </div>\n )}\n\n <div className=\"mt-3 flex flex-wrap items-center gap-2\">\n <CategoryTag category={message.category} />\n {message.actionRequired && <ActionFlag />}\n <span className=\"text-xs text-text-secondary\">{formatDateTime(message.receivedAt)}</span>\n </div>\n\n {confidencePct != null && (\n <div className=\"mt-4 rounded-lg border border-border-subtle bg-bg-surface p-3\">\n <div className=\"flex items-center justify-between text-xs\">\n <span className=\"inline-flex items-center gap-1.5 font-medium text-text-primary\">\n <Sparkles className=\"h-3.5 w-3.5 text-text-link\" /> AI categorization\n </span>\n <span className=\"tabular-nums text-text-secondary\">{confidencePct}% confidence</span>\n </div>\n <MiniProgress\n value={confidencePct}\n max={100}\n tone={confidencePct >= 66 ? 'positive' : confidencePct >= 33 ? 'accent' : 'warning'}\n className=\"mt-2\"\n />\n </div>\n )}\n\n <div className=\"mt-4 border-t border-border-subtle pt-4\">\n {message.preview ? (\n <p className=\"whitespace-pre-wrap text-sm leading-relaxed text-text-primary\">\n {message.preview}\n </p>\n ) : (\n <p className=\"text-sm italic text-text-secondary\">\n No preview available for this message.\n </p>\n )}\n </div>\n\n {message.actionRequired && (\n <div className=\"mt-4 flex items-start gap-2 rounded-lg border border-status-warning-text/30 bg-status-warning-bg-subtle p-3\">\n <ShieldAlert className=\"mt-0.5 h-4 w-4 shrink-0 text-status-warning-text\" />\n <p className=\"text-xs text-status-warning-text\">\n This message needs your attention — review it before its provider deadline.\n </p>\n </div>\n )}\n\n {onViewRelationship && (\n <button\n type=\"button\"\n onClick={onViewRelationship}\n className=\"mt-4 inline-flex items-center gap-1 self-start text-sm font-medium text-text-link hover:underline\"\n >\n View relationship <ChevronRight className=\"h-4 w-4\" />\n </button>\n )}\n </GlassCard>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Filter chip\n// ---------------------------------------------------------------------------\n\nfunction Chip({\n active,\n onClick,\n children,\n tone = 'default',\n}: {\n active: boolean;\n onClick: () => void;\n children: React.ReactNode;\n tone?: 'default' | 'warning';\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-pressed={active}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n active\n ? tone === 'warning'\n ? 'border-transparent bg-status-warning-bg-subtle text-status-warning-text'\n : 'border-transparent bg-action-primary-bg text-action-primary-text'\n : 'border-border-subtle text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n )}\n >\n {children}\n </button>\n );\n}\n\nexport function InboxPage() {\n const { navigateTo } = useSubscriberBot();\n const [page, setPage] = useState(0);\n const [category, setCategory] = useState<'' | SubMessageCategory>('');\n const [unreadOnly, setUnreadOnly] = useState(false);\n const [actionOnly, setActionOnly] = useState(false);\n const [selectedId, setSelectedId] = useState<string | null>(null);\n\n const { data, isLoading, isError, refetch, isFetching } = useInboxMessages({\n category: category || undefined,\n unreadOnly: unreadOnly || undefined,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n const { data: channelsData } = useChannels();\n const markRead = useMarkMessageRead();\n\n const items = data?.items ?? [];\n const hasMore = data?.pageInfo.hasMore ?? false;\n\n // \"Action required\" is a client-side lens over the current page.\n const displayed = actionOnly ? items.filter((m) => m.actionRequired) : items;\n\n // Real, computed page stats.\n const unreadCount = items.filter((m) => !m.isRead).length;\n const actionCount = items.filter((m) => m.actionRequired).length;\n\n const channelMap = new Map((channelsData ?? []).map((c) => [c.id, c]));\n const selected = displayed.find((m) => m.id === selectedId) ?? null;\n const selectedChannel = selected ? channelMap.get(selected.channelId) : undefined;\n\n const resetToFirst = () => {\n setPage(0);\n setSelectedId(null);\n };\n\n return (\n <PageLayout\n title=\"Universal Inbox\"\n description=\"Every subscription communication, merged and categorized\"\n >\n <PagePurpose className=\"mb-4\">\n Subscription communication is noisy and scattered. The Universal Inbox merges every channel\n a provider uses and uses AI to categorize each message so you never miss a renewal, required\n action or security notice.\n </PagePurpose>\n\n {/* Category tabs */}\n <div className=\"-mx-1 flex gap-2 overflow-x-auto px-1 pb-1\">\n {CATEGORY_TABS.map((t) => (\n <span key={t.value} className=\"shrink-0\">\n <Chip\n active={category === t.value}\n onClick={() => {\n setCategory(t.value);\n resetToFirst();\n }}\n >\n {t.label}\n </Chip>\n </span>\n ))}\n </div>\n\n {/* Lenses + stats */}\n <div className=\"mt-3 flex flex-wrap items-center gap-2\">\n <Chip\n active={actionOnly}\n tone=\"warning\"\n onClick={() => {\n setActionOnly((v) => !v);\n setSelectedId(null);\n }}\n >\n <AlertTriangle className=\"h-3.5 w-3.5\" /> Action required\n </Chip>\n <Chip\n active={unreadOnly}\n onClick={() => {\n setUnreadOnly((v) => !v);\n resetToFirst();\n }}\n >\n <Mail className=\"h-3.5 w-3.5\" /> Unread only\n </Chip>\n {!isLoading && !isError && items.length > 0 && (\n <p className=\"ml-auto text-xs text-text-secondary\">\n <span className=\"font-medium text-text-primary tabular-nums\">{unreadCount}</span> unread\n {' · '}\n <span className=\"font-medium text-text-primary tabular-nums\">{actionCount}</span> need\n action\n {isFetching ? ' · refreshing…' : ''}\n </p>\n )}\n </div>\n\n {isLoading ? (\n <LoadingView label=\"Loading inbox…\" />\n ) : isError ? (\n <ErrorView message=\"Could not load the inbox.\" onRetry={() => void refetch()} />\n ) : items.length === 0 ? (\n <div className=\"mt-6\">\n <EmptyState\n illustration=\"empty-inbox\"\n icon={<InboxIcon className=\"h-12 w-12\" />}\n title=\"Inbox is empty\"\n description={\n unreadOnly || category\n ? 'No messages match these filters.'\n : 'Merged subscription communications will appear here once channels start ingesting.'\n }\n />\n </div>\n ) : (\n <div className=\"mt-5 grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_380px]\">\n {/* List */}\n <div className=\"min-w-0\">\n {displayed.length === 0 ? (\n <div className=\"rounded-xl border border-border-subtle bg-bg-surface p-8 text-center\">\n <AlertTriangle className=\"mx-auto h-8 w-8 text-text-secondary\" />\n <p className=\"mt-2 text-sm font-medium text-text-primary\">\n No action-required messages on this page\n </p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n Turn off the “Action required” lens or move to another page.\n </p>\n </div>\n ) : (\n <div className=\"divide-y divide-border-subtle overflow-hidden rounded-xl border border-border-subtle bg-bg-surface\">\n {displayed.map((m) => (\n <MessageRow\n key={m.id}\n message={m}\n active={m.id === selectedId}\n onSelect={() => setSelectedId(m.id)}\n onMarkRead={() => markRead.mutate(m.id)}\n markPending={markRead.isPending}\n onViewRelationship={\n m.relationshipId\n ? () => navigateTo(`/relationships/${m.relationshipId}`)\n : null\n }\n />\n ))}\n </div>\n )}\n\n <div className=\"mt-4 flex items-center justify-between\">\n <p className=\"text-xs text-text-secondary\">\n Page {page + 1}\n {isFetching ? ' · refreshing…' : ''}\n </p>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n disabled={page === 0}\n onClick={() => {\n setPage((p) => Math.max(0, p - 1));\n setSelectedId(null);\n }}\n className={cn(\n 'rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium',\n page === 0\n ? 'cursor-not-allowed text-text-secondary opacity-50'\n : 'text-text-primary hover:bg-bg-sunken'\n )}\n >\n Previous\n </button>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => {\n setPage((p) => p + 1);\n setSelectedId(null);\n }}\n className={cn(\n 'rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium',\n !hasMore\n ? 'cursor-not-allowed text-text-secondary opacity-50'\n : 'text-text-primary hover:bg-bg-sunken'\n )}\n >\n Next\n </button>\n </div>\n </div>\n </div>\n\n {/* Reading pane — wide screens only */}\n <aside className=\"hidden lg:block\">\n <div className=\"lg:sticky lg:top-4\">\n <ReadingPane\n message={selected}\n channel={selectedChannel}\n onMarkRead={() => selected && markRead.mutate(selected.id)}\n markPending={markRead.isPending}\n onViewRelationship={\n selected?.relationshipId\n ? () => navigateTo(`/relationships/${selected.relationshipId}`)\n : null\n }\n />\n </div>\n </aside>\n </div>\n )}\n\n <ChannelsSection />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"InboxPage.js","names":[],"sources":["../../../src/subscriberbot/pages/InboxPage.tsx"],"sourcesContent":["/**\n * Universal Inbox\n * @module subscriberbot/pages\n *\n * Every merged subscription communication in one timeline, AI-categorized,\n * plus management of the ingest channels that feed it. Filter by category /\n * unread, mark messages read, add & verify channels, and log a test message.\n * Reads `subListInboxMessages` / `subListChannels`; writes `subMarkMessageRead`,\n * `subCreateChannel`, `subVerifyChannel`, `subIngestMessage`.\n */\n\nimport { useState } from 'react';\nimport {\n Mail,\n MailOpen,\n AlertTriangle,\n Inbox as InboxIcon,\n Plus,\n Radio,\n BadgeCheck,\n Send,\n MessageSquare,\n MessagesSquare,\n Bell,\n Webhook,\n Phone,\n Hash,\n ShieldAlert,\n Sparkles,\n ChevronRight,\n MousePointerClick,\n type LucideIcon,\n} from 'lucide-react';\nimport { PagePurpose, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { StatusBadge } from '../components/StatusBadge';\nimport { EmptyState } from '../components/EmptyState';\nimport { Modal } from '../components/Modal';\nimport { LoadingView, ErrorView, Section } from '../components/StateViews';\nimport { MiniProgress } from '../components/viz';\nimport { useSubscriberBot } from '../context/SubscriberBotContext';\nimport {\n useInboxMessages,\n useMarkMessageRead,\n useChannels,\n useCreateChannel,\n useVerifyChannel,\n useIngestMessage,\n} from '../hooks/useSubscriberBotApi';\nimport { useTr } from '../hooks/useTr';\nimport { cn } from '../utils/cn';\nimport { formatEnumLabel, formatRelativeTime, formatDateTime } from '../utils/formatters';\nimport type {\n SubChannelType,\n SubCommunicationChannel,\n SubInboxMessage,\n SubMessageCategory,\n} from '../types';\n\nconst PAGE_SIZE = 25;\n\nconst inputCls =\n 'w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\nconst labelCls = 'mb-1 block text-xs font-medium text-text-secondary';\n\nconst MESSAGE_CATEGORIES: SubMessageCategory[] = [\n 'BILLING',\n 'RENEWAL',\n 'REQUIRED_ACTION',\n 'SECURITY_ALERT',\n 'PRODUCT_UPDATE',\n 'MARKETING',\n 'OTHER',\n];\n\nconst CHANNEL_TYPES: SubChannelType[] = [\n 'EMAIL',\n 'SMS',\n 'SLACK',\n 'DISCORD',\n 'WHATSAPP',\n 'PUSH',\n 'WEBHOOK',\n];\n\nconst CATEGORY_KEY: Record<SubMessageCategory, string> = {\n BILLING: 'inbox.categories.billing',\n RENEWAL: 'inbox.categories.renewal',\n REQUIRED_ACTION: 'inbox.categories.requiredAction',\n SECURITY_ALERT: 'inbox.categories.securityAlert',\n PRODUCT_UPDATE: 'inbox.categories.productUpdate',\n MARKETING: 'inbox.categories.marketing',\n OTHER: 'inbox.categories.other',\n};\n\n// Category filter tabs (chips) — the \"All\" pseudo-value plus the real enum.\n// Labels are resolved via useTr inside the page component.\nconst CATEGORY_TAB_VALUES: Array<{\n value: '' | SubMessageCategory;\n key: string;\n fallback: string;\n}> = [\n { value: '', key: 'inbox.categories.all', fallback: 'All' },\n { value: 'BILLING', key: CATEGORY_KEY.BILLING, fallback: 'Billing' },\n { value: 'RENEWAL', key: CATEGORY_KEY.RENEWAL, fallback: 'Renewals' },\n { value: 'REQUIRED_ACTION', key: CATEGORY_KEY.REQUIRED_ACTION, fallback: 'Required actions' },\n { value: 'SECURITY_ALERT', key: CATEGORY_KEY.SECURITY_ALERT, fallback: 'Security' },\n { value: 'PRODUCT_UPDATE', key: CATEGORY_KEY.PRODUCT_UPDATE, fallback: 'Product updates' },\n { value: 'MARKETING', key: CATEGORY_KEY.MARKETING, fallback: 'Marketing' },\n { value: 'OTHER', key: CATEGORY_KEY.OTHER, fallback: 'Other' },\n];\n\n// Per-category tag tone — semantic tokens only.\nconst CATEGORY_TONE: Record<SubMessageCategory, string> = {\n BILLING: 'bg-[var(--color-accent-soft)] text-text-primary',\n RENEWAL: 'bg-status-success-bg-subtle text-status-success-text',\n REQUIRED_ACTION: 'bg-status-warning-bg-subtle text-status-warning-text',\n SECURITY_ALERT: 'bg-status-error-bg-subtle text-status-error-text',\n PRODUCT_UPDATE: 'bg-[var(--color-accent-soft)] text-text-primary',\n MARKETING: 'bg-bg-sunken text-text-secondary',\n OTHER: 'bg-bg-sunken text-text-secondary',\n};\n\nconst CHANNEL_ICON: Record<SubChannelType, LucideIcon> = {\n EMAIL: Mail,\n SMS: MessageSquare,\n SLACK: Hash,\n DISCORD: MessagesSquare,\n WHATSAPP: Phone,\n PUSH: Bell,\n WEBHOOK: Webhook,\n};\n\nfunction CategoryTag({ category }: { category: SubMessageCategory }) {\n return (\n <span\n className={cn(\n 'inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium',\n CATEGORY_TONE[category]\n )}\n >\n {formatEnumLabel(category)}\n </span>\n );\n}\n\nfunction ActionFlag({ className }: { className?: string }) {\n const tr = useTr();\n return (\n <span\n title={tr('inbox.message.actionRequiredTitle', 'Action required')}\n className={cn(\n 'inline-flex items-center gap-1 rounded-full bg-status-warning-bg-subtle px-2 py-0.5 text-[10px] font-medium text-status-warning-text',\n className\n )}\n >\n <AlertTriangle className=\"h-3 w-3\" /> {tr('inbox.message.actionFlag', 'Action')}\n </span>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Add channel modal → subCreateChannel\n// ---------------------------------------------------------------------------\n\nfunction AddChannelModal({ onClose }: { onClose: () => void }) {\n const tr = useTr();\n const create = useCreateChannel();\n const [type, setType] = useState<SubChannelType>('EMAIL');\n const [address, setAddress] = useState('');\n const [label, setLabel] = useState('');\n const [ingestEnabled, setIngestEnabled] = useState(true);\n const [formError, setFormError] = useState<string | null>(null);\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n setFormError(null);\n if (!address.trim()) {\n setFormError(tr('inbox.addChannelModal.addressRequired', 'An address is required.'));\n return;\n }\n create.mutate(\n {\n type,\n address: address.trim(),\n ingestEnabled,\n ...(label.trim() ? { label: label.trim() } : {}),\n },\n {\n onSuccess: () => onClose(),\n onError: (err) =>\n setFormError(\n err instanceof Error\n ? err.message\n : tr('inbox.addChannelModal.error', 'Could not add channel.')\n ),\n }\n );\n };\n\n return (\n <Modal\n title={tr('inbox.addChannelModal.title', 'Add channel')}\n onClose={onClose}\n maxWidthClassName=\"max-w-xl\"\n >\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {formError && (\n <div className=\"rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {formError}\n </div>\n )}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"ch-type\" className={labelCls}>\n {tr('inbox.addChannelModal.typeLabel', 'Type')}\n </label>\n <select\n id=\"ch-type\"\n value={type}\n onChange={(e) => setType(e.target.value as SubChannelType)}\n className={inputCls}\n >\n {CHANNEL_TYPES.map((t) => (\n <option key={t} value={t}>\n {formatEnumLabel(t)}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"ch-label\" className={labelCls}>\n {tr('inbox.addChannelModal.labelLabel', 'Label')}\n </label>\n <input\n id=\"ch-label\"\n type=\"text\"\n value={label}\n onChange={(e) => setLabel(e.target.value)}\n placeholder={tr('inbox.addChannelModal.labelPlaceholder', 'e.g. Billing inbox')}\n className={inputCls}\n />\n </div>\n </div>\n <div>\n <label htmlFor=\"ch-address\" className={labelCls}>\n {tr('inbox.addChannelModal.addressLabel', 'Address')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"ch-address\"\n type=\"text\"\n value={address}\n onChange={(e) => setAddress(e.target.value)}\n placeholder={tr(\n 'inbox.addChannelModal.addressPlaceholder',\n 'e.g. billing@acme.com, +1…, a webhook URL'\n )}\n className={inputCls}\n required\n />\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={ingestEnabled}\n onChange={(e) => setIngestEnabled(e.target.checked)}\n className=\"size-4 rounded border-border-subtle\"\n />\n {tr('inbox.addChannelModal.ingestLabel', 'Ingest messages from this channel')}\n </label>\n <div className=\"flex justify-end gap-3\">\n <button\n type=\"button\"\n onClick={onClose}\n className=\"rounded-lg border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n {tr('inbox.addChannelModal.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={create.isPending}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n {create.isPending\n ? tr('inbox.addChannelModal.adding', 'Adding…')\n : tr('inbox.addChannelModal.submit', 'Add channel')}\n </button>\n </div>\n </form>\n </Modal>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Log test message modal → subIngestMessage\n// ---------------------------------------------------------------------------\n\nfunction LogMessageModal({\n channel,\n onClose,\n}: {\n channel: SubCommunicationChannel;\n onClose: () => void;\n}) {\n const tr = useTr();\n const ingest = useIngestMessage();\n const [subject, setSubject] = useState('');\n const [preview, setPreview] = useState('');\n const [category, setCategory] = useState<SubMessageCategory>('OTHER');\n const [actionRequired, setActionRequired] = useState(false);\n const [formError, setFormError] = useState<string | null>(null);\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n setFormError(null);\n ingest.mutate(\n {\n channelId: channel.id,\n receivedAt: new Date().toISOString(),\n category,\n actionRequired,\n ...(subject.trim() ? { subject: subject.trim() } : {}),\n ...(preview.trim() ? { preview: preview.trim() } : {}),\n },\n {\n onSuccess: () => onClose(),\n onError: (err) =>\n setFormError(\n err instanceof Error\n ? err.message\n : tr('inbox.logMessageModal.error', 'Could not log the message.')\n ),\n }\n );\n };\n\n return (\n <Modal\n title={tr('inbox.logMessageModal.title', 'Log test message')}\n description={tr(\n 'inbox.logMessageModal.description',\n 'Simulate an ingested message from {channel}.',\n { channel: channel.label || channel.address }\n )}\n onClose={onClose}\n >\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {formError && (\n <div className=\"rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {formError}\n </div>\n )}\n <div>\n <label htmlFor=\"msg-subject\" className={labelCls}>\n {tr('inbox.logMessageModal.subjectLabel', 'Subject')}\n </label>\n <input\n id=\"msg-subject\"\n type=\"text\"\n value={subject}\n onChange={(e) => setSubject(e.target.value)}\n placeholder={tr(\n 'inbox.logMessageModal.subjectPlaceholder',\n 'e.g. Your plan renews in 3 days'\n )}\n className={inputCls}\n />\n </div>\n <div>\n <label htmlFor=\"msg-preview\" className={labelCls}>\n {tr('inbox.logMessageModal.previewLabel', 'Preview')}\n </label>\n <textarea\n id=\"msg-preview\"\n value={preview}\n onChange={(e) => setPreview(e.target.value)}\n rows={2}\n className={inputCls}\n />\n </div>\n <div>\n <label htmlFor=\"msg-category\" className={labelCls}>\n {tr('inbox.logMessageModal.categoryLabel', 'Category')}\n </label>\n <select\n id=\"msg-category\"\n value={category}\n onChange={(e) => setCategory(e.target.value as SubMessageCategory)}\n className={inputCls}\n >\n {MESSAGE_CATEGORIES.map((c) => (\n <option key={c} value={c}>\n {tr(CATEGORY_KEY[c], formatEnumLabel(c))}\n </option>\n ))}\n </select>\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={actionRequired}\n onChange={(e) => setActionRequired(e.target.checked)}\n className=\"size-4 rounded border-border-subtle\"\n />\n {tr('inbox.logMessageModal.actionRequired', 'Requires action')}\n </label>\n <div className=\"flex justify-end gap-3\">\n <button\n type=\"button\"\n onClick={onClose}\n className=\"rounded-lg border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n {tr('inbox.logMessageModal.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={ingest.isPending}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n {ingest.isPending\n ? tr('inbox.logMessageModal.logging', 'Logging…')\n : tr('inbox.logMessageModal.submit', 'Log message')}\n </button>\n </div>\n </form>\n </Modal>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Channel card → one per ingest source\n// ---------------------------------------------------------------------------\n\nfunction ChannelCard({\n channel,\n onVerify,\n verifying,\n onLog,\n}: {\n channel: SubCommunicationChannel;\n onVerify: (id: string) => void;\n verifying: boolean;\n onLog: (ch: SubCommunicationChannel) => void;\n}) {\n const tr = useTr();\n const Icon = CHANNEL_ICON[channel.type] ?? Radio;\n const verified = channel.status === 'VERIFIED';\n return (\n <GlassCard treatment=\"metric\" className=\"flex flex-col p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-2.5\">\n <span className=\"flex size-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <Icon className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {formatEnumLabel(channel.type)}\n </p>\n {channel.label && (\n <p className=\"truncate text-xs text-text-secondary\">{channel.label}</p>\n )}\n </div>\n </div>\n <StatusBadge status={channel.status} />\n </div>\n\n <div className=\"mt-3 border-t border-border-subtle pt-3\">\n <p className=\"text-[11px] uppercase tracking-wide text-text-secondary\">\n {channel.ingestEnabled\n ? tr('inbox.channels.ingestingFrom', 'Ingesting from')\n : tr('inbox.channels.ingestPaused', 'Ingest paused')}\n </p>\n <p className=\"mt-0.5 truncate text-sm text-text-primary\" title={channel.address}>\n {channel.address}\n </p>\n </div>\n\n <div className=\"mt-auto flex flex-wrap items-center gap-2 pt-3\">\n {channel.status === 'PENDING' && (\n <button\n type=\"button\"\n disabled={verifying}\n onClick={() => onVerify(channel.id)}\n className=\"inline-flex items-center gap-1 rounded-lg bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n <BadgeCheck className=\"h-3.5 w-3.5\" /> {tr('inbox.channels.verify', 'Verify')}\n </button>\n )}\n {verified && channel.ingestEnabled && (\n <button\n type=\"button\"\n onClick={() => onLog(channel)}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <Send className=\"h-3.5 w-3.5\" />{' '}\n {tr('inbox.channels.logTestMessage', 'Log test message')}\n </button>\n )}\n {channel.status === 'DISABLED' && (\n <span className=\"text-xs text-text-secondary\">\n {tr('inbox.channels.disabled', 'Channel disabled')}\n </span>\n )}\n </div>\n </GlassCard>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Channels management panel\n// ---------------------------------------------------------------------------\n\nfunction ChannelsSection() {\n const tr = useTr();\n const { data, isLoading, isError, refetch } = useChannels();\n const verify = useVerifyChannel();\n const [showAdd, setShowAdd] = useState(false);\n const [logChannel, setLogChannel] = useState<SubCommunicationChannel | null>(null);\n\n const channels = data ?? [];\n const verifiedCount = channels.filter((c) => c.status === 'VERIFIED').length;\n const pendingCount = channels.filter((c) => c.status === 'PENDING').length;\n\n return (\n <Section\n title={tr('inbox.channels.title', 'Ingest channels')}\n description={tr(\n 'inbox.channels.description',\n 'The sources the Universal Inbox pulls subscription communication from'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => setShowAdd(true)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:opacity-90\"\n >\n <Plus className=\"h-3.5 w-3.5\" /> {tr('inbox.channels.add', 'Add channel')}\n </button>\n }\n >\n {isLoading ? (\n <LoadingView label={tr('inbox.channels.loading', 'Loading channels…')} />\n ) : isError ? (\n <ErrorView\n message={tr('inbox.channels.error', 'Could not load channels.')}\n onRetry={() => void refetch()}\n />\n ) : channels.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Radio className=\"h-12 w-12\" />}\n title={tr('inbox.channels.emptyTitle', 'No channels yet')}\n description={tr(\n 'inbox.channels.emptyDescription',\n 'Add an email, SMS, Slack or webhook channel to start ingesting subscription communication.'\n )}\n />\n ) : (\n <>\n {(verifiedCount > 0 || pendingCount > 0) && (\n <div className=\"mb-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"size-1.5 rounded-full bg-status-success-text\" aria-hidden />\n {tr('inbox.channels.verified', '{{count}} verified', { count: verifiedCount })}\n </span>\n {pendingCount > 0 && (\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"size-1.5 rounded-full bg-status-warning-text\" aria-hidden />\n {tr('inbox.channels.pending', '{{count}} pending verification', {\n count: pendingCount,\n })}\n </span>\n )}\n </div>\n )}\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {channels.map((ch) => (\n <ChannelCard\n key={ch.id}\n channel={ch}\n verifying={verify.isPending}\n onVerify={(id) => verify.mutate(id)}\n onLog={(c) => setLogChannel(c)}\n />\n ))}\n </div>\n </>\n )}\n\n {showAdd && <AddChannelModal onClose={() => setShowAdd(false)} />}\n {logChannel && <LogMessageModal channel={logChannel} onClose={() => setLogChannel(null)} />}\n </Section>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Message row (list)\n// ---------------------------------------------------------------------------\n\nfunction MessageRow({\n message,\n active,\n onSelect,\n onMarkRead,\n markPending,\n onViewRelationship,\n}: {\n message: SubInboxMessage;\n active: boolean;\n onSelect: () => void;\n onMarkRead: () => void;\n markPending: boolean;\n onViewRelationship: (() => void) | null;\n}) {\n const tr = useTr();\n const unread = !message.isRead;\n return (\n <div\n role=\"button\"\n tabIndex={0}\n aria-pressed={active}\n onClick={onSelect}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onSelect();\n }\n }}\n className={cn(\n 'relative flex cursor-pointer items-start gap-3 p-4 transition-colors',\n 'focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] focus-visible:ring-inset',\n active ? 'bg-bg-sunken' : unread ? 'bg-accent-wash' : 'hover:bg-bg-sunken/60'\n )}\n >\n {/* unread accent rail */}\n {unread && <span aria-hidden className=\"absolute inset-y-0 left-0 w-0.5 bg-text-link\" />}\n <span className=\"mt-0.5 shrink-0 text-text-secondary\">\n {unread ? <Mail className=\"h-4 w-4 text-text-link\" /> : <MailOpen className=\"h-4 w-4\" />}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2\">\n {unread && (\n <span\n aria-label={tr('inbox.message.unreadAria', 'Unread')}\n className=\"size-1.5 shrink-0 rounded-full bg-text-link\"\n />\n )}\n <p\n className={cn(\n 'min-w-0 flex-1 truncate text-sm',\n unread ? 'font-semibold text-text-primary' : 'font-normal text-text-primary'\n )}\n >\n {message.subject || tr('inbox.message.noSubject', '(no subject)')}\n </p>\n <span className=\"shrink-0 text-xs tabular-nums text-text-secondary\">\n {formatRelativeTime(message.receivedAt)}\n </span>\n </div>\n {message.preview && (\n <p className=\"mt-0.5 line-clamp-2 text-xs text-text-secondary\">{message.preview}</p>\n )}\n <div className=\"mt-1.5 flex flex-wrap items-center gap-2\">\n <CategoryTag category={message.category} />\n {message.actionRequired && <ActionFlag />}\n {onViewRelationship && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onViewRelationship();\n }}\n className=\"text-xs font-medium text-text-link hover:underline\"\n >\n {tr('inbox.message.viewRelationship', 'View relationship')}\n </button>\n )}\n {unread && (\n <button\n type=\"button\"\n disabled={markPending}\n onClick={(e) => {\n e.stopPropagation();\n onMarkRead();\n }}\n className=\"ml-auto shrink-0 rounded-lg border border-border-subtle px-2.5 py-1 text-xs font-medium text-text-primary hover:bg-bg-sunken disabled:opacity-50\"\n >\n {tr('inbox.message.markRead', 'Mark read')}\n </button>\n )}\n </div>\n </div>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Reading pane (wide screens)\n// ---------------------------------------------------------------------------\n\nfunction ReadingPane({\n message,\n channel,\n onMarkRead,\n markPending,\n onViewRelationship,\n}: {\n message: SubInboxMessage | null;\n channel: SubCommunicationChannel | undefined;\n onMarkRead: () => void;\n markPending: boolean;\n onViewRelationship: (() => void) | null;\n}) {\n const tr = useTr();\n if (!message) {\n return (\n <GlassCard\n treatment=\"metric\"\n className=\"flex min-h-[20rem] flex-col items-center justify-center p-8 text-center\"\n >\n <MousePointerClick className=\"h-10 w-10 text-text-secondary\" />\n <p className=\"mt-3 text-sm font-medium text-text-primary\">\n {tr('inbox.readingPane.selectTitle', 'Select a message')}\n </p>\n <p className=\"mt-1 max-w-[16rem] text-xs text-text-secondary\">\n {tr(\n 'inbox.readingPane.selectDescription',\n 'Pick any message from the list to read the full details here.'\n )}\n </p>\n </GlassCard>\n );\n }\n\n const Icon = channel ? (CHANNEL_ICON[channel.type] ?? Radio) : Radio;\n const confidencePct =\n message.categoryConfidence != null\n ? Math.round(Math.max(0, Math.min(1, message.categoryConfidence)) * 100)\n : null;\n\n return (\n <GlassCard treatment=\"metric\" className=\"flex flex-col p-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <h3 className=\"text-base font-semibold text-text-primary\">\n {message.subject || tr('inbox.message.noSubject', '(no subject)')}\n </h3>\n {!message.isRead && (\n <button\n type=\"button\"\n disabled={markPending}\n onClick={onMarkRead}\n className=\"shrink-0 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-primary hover:bg-bg-sunken disabled:opacity-50\"\n >\n {tr('inbox.message.markRead', 'Mark read')}\n </button>\n )}\n </div>\n\n {channel && (\n <div className=\"mt-2 flex items-center gap-2 text-sm\">\n <Icon className=\"h-4 w-4 shrink-0 text-text-secondary\" />\n <span className=\"truncate text-text-secondary\" title={channel.address}>\n {channel.address}\n </span>\n </div>\n )}\n\n <div className=\"mt-3 flex flex-wrap items-center gap-2\">\n <CategoryTag category={message.category} />\n {message.actionRequired && <ActionFlag />}\n <span className=\"text-xs text-text-secondary\">{formatDateTime(message.receivedAt)}</span>\n </div>\n\n {confidencePct != null && (\n <div className=\"mt-4 rounded-lg border border-border-subtle bg-bg-surface p-3\">\n <div className=\"flex items-center justify-between text-xs\">\n <span className=\"inline-flex items-center gap-1.5 font-medium text-text-primary\">\n <Sparkles className=\"h-3.5 w-3.5 text-text-link\" />{' '}\n {tr('inbox.readingPane.aiCategorization', 'AI categorization')}\n </span>\n <span className=\"tabular-nums text-text-secondary\">\n {tr('inbox.readingPane.confidence', '{{pct}}% confidence', { pct: confidencePct })}\n </span>\n </div>\n <MiniProgress\n value={confidencePct}\n max={100}\n tone={confidencePct >= 66 ? 'positive' : confidencePct >= 33 ? 'accent' : 'warning'}\n className=\"mt-2\"\n />\n </div>\n )}\n\n <div className=\"mt-4 border-t border-border-subtle pt-4\">\n {message.preview ? (\n <p className=\"whitespace-pre-wrap text-sm leading-relaxed text-text-primary\">\n {message.preview}\n </p>\n ) : (\n <p className=\"text-sm italic text-text-secondary\">\n {tr('inbox.readingPane.noPreview', 'No preview available for this message.')}\n </p>\n )}\n </div>\n\n {message.actionRequired && (\n <div className=\"mt-4 flex items-start gap-2 rounded-lg border border-status-warning-text/30 bg-status-warning-bg-subtle p-3\">\n <ShieldAlert className=\"mt-0.5 h-4 w-4 shrink-0 text-status-warning-text\" />\n <p className=\"text-xs text-status-warning-text\">\n {tr(\n 'inbox.readingPane.attention',\n 'This message needs your attention — review it before its provider deadline.'\n )}\n </p>\n </div>\n )}\n\n {onViewRelationship && (\n <button\n type=\"button\"\n onClick={onViewRelationship}\n className=\"mt-4 inline-flex items-center gap-1 self-start text-sm font-medium text-text-link hover:underline\"\n >\n {tr('inbox.message.viewRelationship', 'View relationship')}{' '}\n <ChevronRight className=\"h-4 w-4\" />\n </button>\n )}\n </GlassCard>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Filter chip\n// ---------------------------------------------------------------------------\n\nfunction Chip({\n active,\n onClick,\n children,\n tone = 'default',\n}: {\n active: boolean;\n onClick: () => void;\n children: React.ReactNode;\n tone?: 'default' | 'warning';\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-pressed={active}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n active\n ? tone === 'warning'\n ? 'border-transparent bg-status-warning-bg-subtle text-status-warning-text'\n : 'border-transparent bg-action-primary-bg text-action-primary-text'\n : 'border-border-subtle text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n )}\n >\n {children}\n </button>\n );\n}\n\nexport function InboxPage() {\n const tr = useTr();\n const { navigateTo } = useSubscriberBot();\n const [page, setPage] = useState(0);\n const [category, setCategory] = useState<'' | SubMessageCategory>('');\n const [unreadOnly, setUnreadOnly] = useState(false);\n const [actionOnly, setActionOnly] = useState(false);\n const [selectedId, setSelectedId] = useState<string | null>(null);\n\n const { data, isLoading, isError, refetch, isFetching } = useInboxMessages({\n category: category || undefined,\n unreadOnly: unreadOnly || undefined,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n const { data: channelsData } = useChannels();\n const markRead = useMarkMessageRead();\n\n const items = data?.items ?? [];\n const hasMore = data?.pageInfo.hasMore ?? false;\n\n // \"Action required\" is a client-side lens over the current page.\n const displayed = actionOnly ? items.filter((m) => m.actionRequired) : items;\n\n // Real, computed page stats.\n const unreadCount = items.filter((m) => !m.isRead).length;\n const actionCount = items.filter((m) => m.actionRequired).length;\n\n const channelMap = new Map((channelsData ?? []).map((c) => [c.id, c]));\n const selected = displayed.find((m) => m.id === selectedId) ?? null;\n const selectedChannel = selected ? channelMap.get(selected.channelId) : undefined;\n\n const resetToFirst = () => {\n setPage(0);\n setSelectedId(null);\n };\n\n return (\n <PageLayout\n title={tr('inbox.title', 'Universal Inbox')}\n description={tr(\n 'inbox.description',\n 'Every subscription communication, merged and categorized'\n )}\n >\n <PagePurpose className=\"mb-4\">\n {tr(\n 'inbox.purpose',\n 'Subscription communication is noisy and scattered. The Universal Inbox merges every channel a provider uses and uses AI to categorize each message so you never miss a renewal, required action or security notice.'\n )}\n </PagePurpose>\n\n {/* Category tabs */}\n <div className=\"-mx-1 flex gap-2 overflow-x-auto px-1 pb-1\">\n {CATEGORY_TAB_VALUES.map((t) => (\n <span key={t.value} className=\"shrink-0\">\n <Chip\n active={category === t.value}\n onClick={() => {\n setCategory(t.value);\n resetToFirst();\n }}\n >\n {tr(t.key, t.fallback)}\n </Chip>\n </span>\n ))}\n </div>\n\n {/* Lenses + stats */}\n <div className=\"mt-3 flex flex-wrap items-center gap-2\">\n <Chip\n active={actionOnly}\n tone=\"warning\"\n onClick={() => {\n setActionOnly((v) => !v);\n setSelectedId(null);\n }}\n >\n <AlertTriangle className=\"h-3.5 w-3.5\" />{' '}\n {tr('inbox.filters.actionRequired', 'Action required')}\n </Chip>\n <Chip\n active={unreadOnly}\n onClick={() => {\n setUnreadOnly((v) => !v);\n resetToFirst();\n }}\n >\n <Mail className=\"h-3.5 w-3.5\" /> {tr('inbox.filters.unreadOnly', 'Unread only')}\n </Chip>\n {!isLoading && !isError && items.length > 0 && (\n <p className=\"ml-auto text-xs text-text-secondary\">\n {tr('inbox.filters.unreadCount', '{{count}} unread', { count: unreadCount })}\n {' · '}\n {tr('inbox.filters.actionCount', '{{count}} need action', { count: actionCount })}\n {isFetching ? ` · ${tr('inbox.filters.refreshing', 'refreshing…')}` : ''}\n </p>\n )}\n </div>\n\n {isLoading ? (\n <LoadingView label={tr('inbox.loading', 'Loading inbox…')} />\n ) : isError ? (\n <ErrorView\n message={tr('inbox.error', 'Could not load the inbox.')}\n onRetry={() => void refetch()}\n />\n ) : items.length === 0 ? (\n <div className=\"mt-6\">\n <EmptyState\n illustration=\"empty-inbox\"\n icon={<InboxIcon className=\"h-12 w-12\" />}\n title={tr('inbox.emptyTitle', 'Inbox is empty')}\n description={\n unreadOnly || category\n ? tr('inbox.emptyDescriptionFiltered', 'No messages match these filters.')\n : tr(\n 'inbox.emptyDescription',\n 'Merged subscription communications will appear here once channels start ingesting.'\n )\n }\n />\n </div>\n ) : (\n <div className=\"mt-5 grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_380px]\">\n {/* List */}\n <div className=\"min-w-0\">\n {displayed.length === 0 ? (\n <div className=\"rounded-xl border border-border-subtle bg-bg-surface p-8 text-center\">\n <AlertTriangle className=\"mx-auto h-8 w-8 text-text-secondary\" />\n <p className=\"mt-2 text-sm font-medium text-text-primary\">\n {tr('inbox.noActionTitle', 'No action-required messages on this page')}\n </p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {tr(\n 'inbox.noActionDescription',\n 'Turn off the “Action required” lens or move to another page.'\n )}\n </p>\n </div>\n ) : (\n <div className=\"divide-y divide-border-subtle overflow-hidden rounded-xl border border-border-subtle bg-bg-surface\">\n {displayed.map((m) => (\n <MessageRow\n key={m.id}\n message={m}\n active={m.id === selectedId}\n onSelect={() => setSelectedId(m.id)}\n onMarkRead={() => markRead.mutate(m.id)}\n markPending={markRead.isPending}\n onViewRelationship={\n m.relationshipId\n ? () => navigateTo(`/relationships/${m.relationshipId}`)\n : null\n }\n />\n ))}\n </div>\n )}\n\n <div className=\"mt-4 flex items-center justify-between\">\n <p className=\"text-xs text-text-secondary\">\n {tr('inbox.pagination.page', 'Page {{page}}', { page: page + 1 })}\n {isFetching ? ` · ${tr('inbox.filters.refreshing', 'refreshing…')}` : ''}\n </p>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n disabled={page === 0}\n onClick={() => {\n setPage((p) => Math.max(0, p - 1));\n setSelectedId(null);\n }}\n className={cn(\n 'rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium',\n page === 0\n ? 'cursor-not-allowed text-text-secondary opacity-50'\n : 'text-text-primary hover:bg-bg-sunken'\n )}\n >\n {tr('inbox.pagination.previous', 'Previous')}\n </button>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => {\n setPage((p) => p + 1);\n setSelectedId(null);\n }}\n className={cn(\n 'rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium',\n !hasMore\n ? 'cursor-not-allowed text-text-secondary opacity-50'\n : 'text-text-primary hover:bg-bg-sunken'\n )}\n >\n {tr('inbox.pagination.next', 'Next')}\n </button>\n </div>\n </div>\n </div>\n\n {/* Reading pane — wide screens only */}\n <aside className=\"hidden lg:block\">\n <div className=\"lg:sticky lg:top-4\">\n <ReadingPane\n message={selected}\n channel={selectedChannel}\n onMarkRead={() => selected && markRead.mutate(selected.id)}\n markPending={markRead.isPending}\n onViewRelationship={\n selected?.relationshipId\n ? () => navigateTo(`/relationships/${selected.relationshipId}`)\n : null\n }\n />\n </div>\n </aside>\n </div>\n )}\n\n <ChannelsSection />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"InsightsPage.d.ts","sourceRoot":"","sources":["../../../src/subscriberbot/pages/InsightsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAW,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAkCzC,eAAO,MAAM,YAAY,EAAE,EA8M1B,CAAC"}
1
+ {"version":3,"file":"InsightsPage.d.ts","sourceRoot":"","sources":["../../../src/subscriberbot/pages/InsightsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAW,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAmCzC,eAAO,MAAM,YAAY,EAAE,EAyO1B,CAAC"}
@@ -7,12 +7,13 @@ import { ErrorView as o, LoadingView as s, Section as c } from "../components/St
7
7
  import { BarList as l, Donut as u, Legend as d, MiniProgress as f } from "../components/viz.js";
8
8
  import "../components/index.js";
9
9
  import { useRelationships as p } from "../hooks/useSubscriberBotApi.js";
10
- import { useMemo as m } from "react";
11
- import { CalendarClock as h, HeartPulse as g, Layers as _, TrendingUp as v, Wallet as y } from "lucide-react";
12
- import { Fragment as b, jsx as x, jsxs as S } from "react/jsx-runtime";
13
- import { GlassCard as C, PagePurpose as w } from "@burdenoff/fe-libs/ui";
10
+ import { useTr as m } from "../hooks/useTr.js";
11
+ import { useMemo as h } from "react";
12
+ import { CalendarClock as g, HeartPulse as _, Layers as v, TrendingUp as y, Wallet as b } from "lucide-react";
13
+ import { Fragment as x, jsx as S, jsxs as C } from "react/jsx-runtime";
14
+ import { GlassCard as w, PagePurpose as T } from "@burdenoff/fe-libs/ui";
14
15
  //#region src/subscriberbot/pages/InsightsPage.tsx
15
- var T = {
16
+ var E = {
16
17
  DAILY: 30,
17
18
  WEEKLY: 4.345,
18
19
  MONTHLY: 1,
@@ -21,15 +22,15 @@ var T = {
21
22
  LIFETIME: 0,
22
23
  FREE: 0,
23
24
  USAGE_BASED: 1
24
- }, E = (e) => e.amount * (T[e.interval] ?? 1), D = (e) => e.status === "ACTIVE", O = () => {
25
- let T = p({ pagination: { limit: 100 } }), O = m(() => T.data?.items ?? [], [T.data]), k = m(() => {
26
- let e = O.filter(D), t = e[0]?.currency ?? O[0]?.currency ?? "USD", n = e.reduce((e, t) => e + E(t), 0), r = O.filter((e) => e.healthScore != null), a = r.length ? r.reduce((e, t) => e + (t.healthScore ?? 0), 0) / r.length : null, o = /* @__PURE__ */ new Map();
27
- for (let e of O) o.set(e.status, (o.get(e.status) ?? 0) + 1);
25
+ }, D = (e) => e.amount * (E[e.interval] ?? 1), O = (e) => e.status === "ACTIVE", k = () => {
26
+ let E = p({ pagination: { limit: 100 } }), k = m(), A = h(() => E.data?.items ?? [], [E.data]), j = h(() => {
27
+ let e = A.filter(O), t = e[0]?.currency ?? A[0]?.currency ?? "USD", n = e.reduce((e, t) => e + D(t), 0), r = A.filter((e) => e.healthScore != null), a = r.length ? r.reduce((e, t) => e + (t.healthScore ?? 0), 0) / r.length : null, o = /* @__PURE__ */ new Map();
28
+ for (let e of A) o.set(e.status, (o.get(e.status) ?? 0) + 1);
28
29
  let s = /* @__PURE__ */ new Map();
29
- for (let t of e) s.set(t.kind, (s.get(t.kind) ?? 0) + E(t));
30
- let c = [...e].sort((e, t) => E(t) - E(e)).slice(0, 6), l = [], u = /* @__PURE__ */ new Date();
30
+ for (let t of e) s.set(t.kind, (s.get(t.kind) ?? 0) + D(t));
31
+ let c = [...e].sort((e, t) => D(t) - D(e)).slice(0, 6), l = [], u = /* @__PURE__ */ new Date();
31
32
  for (let e = 0; e < 6; e += 1) {
32
- let t = new Date(u.getFullYear(), u.getMonth() + e, 1), n = `${t.getFullYear()}-${t.getMonth()}`, r = t.toLocaleString(void 0, { month: "short" }), i = O.reduce((e, t) => {
33
+ let t = new Date(u.getFullYear(), u.getMonth() + e, 1), n = `${t.getFullYear()}-${t.getMonth()}`, r = t.toLocaleString(void 0, { month: "short" }), i = A.reduce((e, t) => {
33
34
  if (!t.renewsAt) return e;
34
35
  let r = new Date(t.renewsAt);
35
36
  return Number.isNaN(r.getTime()) ? e : `${r.getFullYear()}-${r.getMonth()}` === n ? e + t.amount : e;
@@ -39,7 +40,7 @@ var T = {
39
40
  value: i
40
41
  });
41
42
  }
42
- let d = O.filter((e) => e.healthScore != null && e.healthScore < 40).sort((e, t) => (e.healthScore ?? 0) - (t.healthScore ?? 0)).slice(0, 6), f = O.filter((e) => {
43
+ let d = A.filter((e) => e.healthScore != null && e.healthScore < 40).sort((e, t) => (e.healthScore ?? 0) - (t.healthScore ?? 0)).slice(0, 6), f = A.filter((e) => {
43
44
  if (!e.renewsAt) return !1;
44
45
  let t = new Date(e.renewsAt).getTime();
45
46
  if (Number.isNaN(t)) return !1;
@@ -63,151 +64,151 @@ var T = {
63
64
  topCosts: c,
64
65
  forecast: l,
65
66
  watchlist: d,
66
- total: O.length
67
+ total: A.length
67
68
  };
68
- }, [O]);
69
- if (T.isLoading) return /* @__PURE__ */ x(s, { label: "Crunching your spend insights…" });
70
- if (T.isError) return /* @__PURE__ */ x(o, {
71
- message: "Could not load insights.",
72
- onRetry: () => void T.refetch()
69
+ }, [A]);
70
+ if (E.isLoading) return /* @__PURE__ */ S(s, { label: k("insights.loading", "Crunching your spend insights…") });
71
+ if (E.isError) return /* @__PURE__ */ S(o, {
72
+ message: k("insights.error", "Could not load insights."),
73
+ onRetry: () => void E.refetch()
73
74
  });
74
- let A = (e) => r(e, k.currency);
75
- return /* @__PURE__ */ S(t, {
76
- title: "Insights",
77
- description: "Where your recurring money goes, what renews next, and what to review — computed live from your portfolio.",
78
- children: [/* @__PURE__ */ x(w, {
75
+ let M = (e) => r(e, j.currency);
76
+ return /* @__PURE__ */ C(t, {
77
+ title: k("insights.title", "Insights"),
78
+ description: k("insights.description", "Where your recurring money goes, what renews next, and what to review — computed live from your portfolio."),
79
+ children: [/* @__PURE__ */ S(T, {
79
80
  className: "mb-2",
80
- children: "Understand and optimize your recurring spend. Every figure here is aggregated from your real subscriptions — normalized to a monthly equivalent so plans on different billing cycles are comparable."
81
- }), k.total === 0 ? /* @__PURE__ */ x(a, {
82
- icon: /* @__PURE__ */ x(v, { className: "h-8 w-8" }),
83
- title: "No data to analyze yet",
84
- description: "Add a few subscriptions and your spend breakdown, renewal forecast and health watchlist will appear here."
85
- }) : /* @__PURE__ */ S(b, { children: [
86
- /* @__PURE__ */ S("div", {
81
+ children: k("insights.purpose", "Understand and optimize your recurring spend. Every figure here is aggregated from your real subscriptions — normalized to a monthly equivalent so plans on different billing cycles are comparable.")
82
+ }), j.total === 0 ? /* @__PURE__ */ S(a, {
83
+ icon: /* @__PURE__ */ S(y, { className: "h-8 w-8" }),
84
+ title: k("insights.emptyTitle", "No data to analyze yet"),
85
+ description: k("insights.emptyDescription", "Add a few subscriptions and your spend breakdown, renewal forecast and health watchlist will appear here.")
86
+ }) : /* @__PURE__ */ C(x, { children: [
87
+ /* @__PURE__ */ C("div", {
87
88
  className: "grid grid-cols-2 gap-4 lg:grid-cols-4",
88
89
  children: [
89
- /* @__PURE__ */ x(n, {
90
- icon: /* @__PURE__ */ x(y, { className: "h-5 w-5" }),
91
- value: A(k.monthlySpend),
92
- label: "Recurring spend / month",
90
+ /* @__PURE__ */ S(n, {
91
+ icon: /* @__PURE__ */ S(b, { className: "h-5 w-5" }),
92
+ value: M(j.monthlySpend),
93
+ label: k("insights.metrics.recurringSpend", "Recurring spend / month"),
93
94
  featured: !0
94
95
  }),
95
- /* @__PURE__ */ x(n, {
96
- icon: /* @__PURE__ */ x(_, { className: "h-5 w-5" }),
97
- value: k.activeCount,
98
- label: "Active subscriptions"
96
+ /* @__PURE__ */ S(n, {
97
+ icon: /* @__PURE__ */ S(v, { className: "h-5 w-5" }),
98
+ value: j.activeCount,
99
+ label: k("insights.metrics.activeSubscriptions", "Active subscriptions")
99
100
  }),
100
- /* @__PURE__ */ x(n, {
101
- icon: /* @__PURE__ */ x(h, { className: "h-5 w-5" }),
102
- value: k.renewals30,
103
- label: "Renewing in 30 days"
101
+ /* @__PURE__ */ S(n, {
102
+ icon: /* @__PURE__ */ S(g, { className: "h-5 w-5" }),
103
+ value: j.renewals30,
104
+ label: k("insights.metrics.renewing30Days", "Renewing in 30 days")
104
105
  }),
105
- /* @__PURE__ */ x(n, {
106
- icon: /* @__PURE__ */ x(g, { className: "h-5 w-5" }),
107
- value: k.avgHealth == null ? "—" : Math.round(k.avgHealth),
108
- label: "Avg. health score"
106
+ /* @__PURE__ */ S(n, {
107
+ icon: /* @__PURE__ */ S(_, { className: "h-5 w-5" }),
108
+ value: j.avgHealth == null ? k("insights.metrics.noScore", "—") : Math.round(j.avgHealth),
109
+ label: k("insights.metrics.avgHealthScore", "Avg. health score")
109
110
  })
110
111
  ]
111
112
  }),
112
- /* @__PURE__ */ S("div", {
113
+ /* @__PURE__ */ C("div", {
113
114
  className: "grid grid-cols-1 gap-4 lg:grid-cols-3",
114
115
  children: [
115
- /* @__PURE__ */ S(C, {
116
+ /* @__PURE__ */ C(w, {
116
117
  treatment: "plain",
117
118
  className: "p-6",
118
- children: [/* @__PURE__ */ x("h3", {
119
+ children: [/* @__PURE__ */ S("h3", {
119
120
  className: "mb-4 text-sm font-semibold text-text-primary",
120
- children: "Portfolio by status"
121
- }), /* @__PURE__ */ S("div", {
121
+ children: k("insights.portfolioByStatus", "Portfolio by status")
122
+ }), /* @__PURE__ */ C("div", {
122
123
  className: "flex flex-col items-center gap-6 sm:flex-row",
123
- children: [/* @__PURE__ */ x(u, {
124
- data: k.statusData,
125
- centerLabel: String(k.total),
126
- centerSub: "subscriptions"
127
- }), /* @__PURE__ */ x(d, {
124
+ children: [/* @__PURE__ */ S(u, {
125
+ data: j.statusData,
126
+ centerLabel: String(j.total),
127
+ centerSub: k("insights.subscriptions", "subscriptions")
128
+ }), /* @__PURE__ */ S(d, {
128
129
  className: "w-full sm:flex-1",
129
- data: k.statusData
130
+ data: j.statusData
130
131
  })]
131
132
  })]
132
133
  }),
133
- /* @__PURE__ */ S(C, {
134
+ /* @__PURE__ */ C(w, {
134
135
  treatment: "plain",
135
136
  className: "p-6",
136
- children: [/* @__PURE__ */ x("h3", {
137
+ children: [/* @__PURE__ */ S("h3", {
137
138
  className: "mb-4 text-sm font-semibold text-text-primary",
138
- children: "Monthly spend by type"
139
- }), k.kindData.length ? /* @__PURE__ */ x(l, {
140
- data: k.kindData,
141
- valueFormat: A
142
- }) : /* @__PURE__ */ x("p", {
139
+ children: k("insights.spendByType", "Monthly spend by type")
140
+ }), j.kindData.length ? /* @__PURE__ */ S(l, {
141
+ data: j.kindData,
142
+ valueFormat: M
143
+ }) : /* @__PURE__ */ S("p", {
143
144
  className: "text-sm text-text-secondary",
144
- children: "No active spend."
145
+ children: k("insights.noActiveSpend", "No active spend.")
145
146
  })]
146
147
  }),
147
- /* @__PURE__ */ S(C, {
148
+ /* @__PURE__ */ C(w, {
148
149
  treatment: "plain",
149
150
  className: "p-6",
150
- children: [/* @__PURE__ */ x("h3", {
151
+ children: [/* @__PURE__ */ S("h3", {
151
152
  className: "mb-4 text-sm font-semibold text-text-primary",
152
- children: "Renewal forecast (6 mo)"
153
- }), k.forecast.some((e) => e.value > 0) ? /* @__PURE__ */ x(l, {
154
- data: k.forecast,
155
- valueFormat: A
156
- }) : /* @__PURE__ */ x("p", {
153
+ children: k("insights.renewalForecast", "Renewal forecast (6 mo)")
154
+ }), j.forecast.some((e) => e.value > 0) ? /* @__PURE__ */ S(l, {
155
+ data: j.forecast,
156
+ valueFormat: M
157
+ }) : /* @__PURE__ */ S("p", {
157
158
  className: "text-sm text-text-secondary",
158
- children: "No dated renewals in the next 6 months."
159
+ children: k("insights.noRenewals", "No dated renewals in the next 6 months.")
159
160
  })]
160
161
  })
161
162
  ]
162
163
  }),
163
- /* @__PURE__ */ S("div", {
164
+ /* @__PURE__ */ C("div", {
164
165
  className: "grid grid-cols-1 gap-4 lg:grid-cols-2",
165
- children: [/* @__PURE__ */ x(c, {
166
- title: "Top monthly costs",
167
- children: /* @__PURE__ */ x(C, {
166
+ children: [/* @__PURE__ */ S(c, {
167
+ title: k("insights.topCosts", "Top monthly costs"),
168
+ children: /* @__PURE__ */ S(w, {
168
169
  treatment: "plain",
169
170
  className: "divide-y divide-border-subtle",
170
- children: k.topCosts.map((e) => /* @__PURE__ */ S("div", {
171
+ children: j.topCosts.map((e) => /* @__PURE__ */ C("div", {
171
172
  className: "flex items-center justify-between p-4",
172
- children: [/* @__PURE__ */ x("span", {
173
+ children: [/* @__PURE__ */ S("span", {
173
174
  className: "truncate text-sm text-text-primary",
174
175
  children: e.label
175
- }), /* @__PURE__ */ S("span", {
176
+ }), /* @__PURE__ */ C("span", {
176
177
  className: "ml-3 shrink-0 text-sm font-semibold tabular-nums text-text-primary",
177
- children: [A(E(e)), /* @__PURE__ */ x("span", {
178
+ children: [M(D(e)), /* @__PURE__ */ S("span", {
178
179
  className: "text-xs font-normal text-text-secondary",
179
- children: "/mo"
180
+ children: k("insights.perMonth", "/mo")
180
181
  })]
181
182
  })]
182
183
  }, e.id))
183
184
  })
184
- }), /* @__PURE__ */ x(c, {
185
- title: "Health watchlist",
186
- children: k.watchlist.length ? /* @__PURE__ */ x(C, {
185
+ }), /* @__PURE__ */ S(c, {
186
+ title: k("insights.healthWatchlist", "Health watchlist"),
187
+ children: j.watchlist.length ? /* @__PURE__ */ S(w, {
187
188
  treatment: "plain",
188
189
  className: "divide-y divide-border-subtle",
189
- children: k.watchlist.map((t) => /* @__PURE__ */ S("div", {
190
+ children: j.watchlist.map((t) => /* @__PURE__ */ C("div", {
190
191
  className: "flex items-center gap-3 p-4",
191
192
  children: [
192
- /* @__PURE__ */ x("span", {
193
+ /* @__PURE__ */ S("span", {
193
194
  className: "w-32 shrink-0 truncate text-sm text-text-primary",
194
195
  children: t.label
195
196
  }),
196
- /* @__PURE__ */ x(f, {
197
+ /* @__PURE__ */ S(f, {
197
198
  className: "flex-1",
198
199
  value: t.healthScore ?? 0,
199
200
  tone: "negative"
200
201
  }),
201
- /* @__PURE__ */ x("span", {
202
+ /* @__PURE__ */ S("span", {
202
203
  className: e("w-8 shrink-0 text-right text-sm font-medium tabular-nums text-status-warning-text"),
203
204
  children: t.healthScore
204
205
  })
205
206
  ]
206
207
  }, t.id))
207
- }) : /* @__PURE__ */ x(C, {
208
+ }) : /* @__PURE__ */ S(w, {
208
209
  treatment: "plain",
209
210
  className: "p-6 text-sm text-text-secondary",
210
- children: "Nothing needs review — all scored subscriptions look healthy."
211
+ children: k("insights.allHealthy", "Nothing needs review — all scored subscriptions look healthy.")
211
212
  })
212
213
  })]
213
214
  })
@@ -215,6 +216,6 @@ var T = {
215
216
  });
216
217
  };
217
218
  //#endregion
218
- export { O as InsightsPage };
219
+ export { k as InsightsPage };
219
220
 
220
221
  //# sourceMappingURL=InsightsPage.js.map