@burdenoff/microfe-subscriberbot 2026.1029.1 → 2026.1030.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.
@@ -1 +1 @@
1
- {"version":3,"file":"WorkflowsPage.js","names":[],"sources":["../../../src/subscriberbot/pages/WorkflowsPage.tsx"],"sourcesContent":["/**\n * Workflows (Automation)\n * @module subscriberbot/pages\n *\n * The Workflow Engine — event-driven automation (\"when invoice arrives →\n * classify → update relationship → notify finance\"). List + create + activate.\n * Reads `subListWorkflows`; writes `subCreateWorkflow` / `subActivateWorkflow`.\n *\n * Opening a workflow renders a READ-ONLY horizontal step-flow derived from the\n * workflow's real `triggerEvent`, `name` and `status` (the svc contract does\n * not expose an editable step array — this is a truthful visualisation, not a\n * builder).\n */\n\nimport { useState } from 'react';\nimport {\n Plus,\n Workflow as WorkflowIcon,\n Play,\n Zap,\n Sparkles,\n CheckCircle2,\n ArrowRight,\n Clock,\n X,\n} from 'lucide-react';\nimport { PagePurpose, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n EmptyState,\n Modal,\n MetricsCard,\n LoadingView,\n ErrorView,\n} from '../components';\nimport { useWorkflows, useCreateWorkflow, useActivateWorkflow } from '../hooks/useSubscriberBotApi';\nimport { useTr } from '../hooks/useTr';\nimport { formatRelativeTime } from '../utils/formatters';\nimport type { SubCreateWorkflowInput, SubWorkflow, SubWorkflowStatus } from '../types';\n\nconst STATUS_OPTIONS: Array<{ value: '' | SubWorkflowStatus; labelKey: string; fallback: string }> =\n [\n { value: '', labelKey: 'workflows.filters.allStatuses', fallback: 'All statuses' },\n { value: 'ACTIVE', labelKey: 'workflows.filters.active', fallback: 'Active' },\n { value: 'DRAFT', labelKey: 'workflows.filters.draft', fallback: 'Draft' },\n { value: 'PAUSED', labelKey: 'workflows.filters.paused', fallback: 'Paused' },\n { value: 'ARCHIVED', labelKey: 'workflows.filters.archived', fallback: 'Archived' },\n ];\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// ---------------------------------------------------------------------------\n// A single node in the read-only step flow.\n// ---------------------------------------------------------------------------\nfunction FlowNode({\n icon,\n kicker,\n title,\n sub,\n accent,\n}: {\n icon: React.ReactNode;\n kicker: string;\n title: string;\n sub?: string;\n /** Top accent bar tone. */\n accent: 'trigger' | 'run' | 'done';\n}) {\n const bar =\n accent === 'done'\n ? 'bg-status-success-text'\n : accent === 'run'\n ? 'bg-status-warning-text'\n : 'bg-[var(--color-accent-indigo)]';\n return (\n <div className=\"w-48 shrink-0 overflow-hidden rounded-xl border border-border-subtle bg-bg-surface shadow-sm\">\n <div className={`h-1 w-full ${bar}`} aria-hidden />\n <div className=\"p-3\">\n <span className=\"flex size-8 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n {icon}\n </span>\n <p className=\"mt-2 text-[10px] font-medium uppercase tracking-wide text-text-secondary\">\n {kicker}\n </p>\n <p className=\"truncate text-sm font-semibold text-text-primary\" title={title}>\n {title}\n </p>\n {sub && <p className=\"mt-0.5 truncate text-xs text-text-secondary\">{sub}</p>}\n </div>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Read-only flow panel for the selected workflow.\n// ---------------------------------------------------------------------------\nfunction WorkflowFlowPanel({\n workflow,\n onClose,\n onActivate,\n activating,\n}: {\n workflow: SubWorkflow;\n onClose: () => void;\n onActivate: () => void;\n activating: boolean;\n}) {\n const tr = useTr();\n const connector = (\n <div className=\"flex shrink-0 items-center px-1 text-text-secondary\" aria-hidden>\n <span className=\"h-px w-4 bg-border-seam sm:w-6\" />\n <ArrowRight className=\"h-4 w-4\" />\n </div>\n );\n\n const statusLabel = workflow.status.charAt(0) + workflow.status.slice(1).toLowerCase();\n\n return (\n <GlassCard treatment=\"glass\" glow className=\"mt-6 p-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <p className=\"text-base font-semibold text-text-primary\">{workflow.name}</p>\n <StatusBadge status={workflow.status} />\n </div>\n <p className=\"mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-text-secondary\">\n <span className=\"tabular-nums\">\n {tr('workflows.flow.ran', 'Ran {{count}}×', { count: workflow.runCount })}\n </span>\n {workflow.lastRunAt && (\n <span className=\"inline-flex items-center gap-1\">\n <Clock className=\"h-3 w-3\" aria-hidden />{' '}\n {tr('workflows.flow.lastRun', 'last {{when}}', {\n when: formatRelativeTime(workflow.lastRunAt),\n })}\n </span>\n )}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label={tr('workflows.flow.closeAriaLabel', 'Close flow')}\n className=\"rounded-lg border border-border-subtle p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n\n {workflow.description && (\n <p className=\"mt-2 max-w-3xl text-sm text-text-secondary\">{workflow.description}</p>\n )}\n\n {/* Horizontal read-only step flow (scrolls on small screens) */}\n <div className=\"mt-4 overflow-x-auto pb-2\">\n <div className=\"flex min-w-max items-stretch\">\n <FlowNode\n accent=\"trigger\"\n kicker={tr('workflows.flow.trigger', 'When')}\n icon={<Zap className=\"h-4 w-4\" />}\n title={workflow.triggerEvent}\n sub={tr('workflows.flow.triggerEvent', 'Trigger event')}\n />\n {connector}\n <FlowNode\n accent=\"run\"\n kicker={tr('workflows.flow.run', 'Run')}\n icon={<Sparkles className=\"h-4 w-4\" />}\n title={workflow.name}\n sub={tr('workflows.flow.automation', 'Automation')}\n />\n {connector}\n <FlowNode\n accent=\"done\"\n kicker={tr('workflows.flow.then', 'Then')}\n icon={<CheckCircle2 className=\"h-4 w-4\" />}\n title={\n workflow.status === 'ACTIVE'\n ? tr('workflows.flow.live', 'Live')\n : tr('workflows.flow.awaitingActivation', 'Awaiting activation')\n }\n sub={tr('workflows.flow.status', 'Status: {{status}}', { status: statusLabel })}\n />\n </div>\n </div>\n\n {workflow.status !== 'ACTIVE' && (\n <div className=\"mt-3 flex justify-end\">\n <button\n type=\"button\"\n disabled={activating}\n onClick={onActivate}\n className=\"inline-flex items-center gap-1.5 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 <Play className=\"h-4 w-4\" />{' '}\n {activating\n ? tr('common.activating', 'Activating…')\n : tr('workflows.flow.activate', 'Activate workflow')}\n </button>\n </div>\n )}\n </GlassCard>\n );\n}\n\nexport function WorkflowsPage() {\n const [statusFilter, setStatusFilter] = useState<'' | SubWorkflowStatus>('');\n const [showCreate, setShowCreate] = useState(false);\n const [selectedId, setSelectedId] = useState<string | null>(null);\n const tr = useTr();\n\n const { data, isLoading, isError, refetch } = useWorkflows(statusFilter || undefined);\n const create = useCreateWorkflow();\n const activate = useActivateWorkflow();\n\n const [name, setName] = useState('');\n const [triggerEvent, setTriggerEvent] = useState('');\n const [description, setDescription] = useState('');\n const [formError, setFormError] = useState<string | null>(null);\n\n const workflows = data ?? [];\n const selected = workflows.find((w) => w.id === selectedId) ?? null;\n\n // --- Real, computed aggregates -------------------------------------------\n const activeCount = workflows.filter((w) => w.status === 'ACTIVE').length;\n const totalRuns = workflows.reduce((s, w) => s + (w.runCount || 0), 0);\n\n const resetForm = () => {\n setName('');\n setTriggerEvent('');\n setDescription('');\n setFormError(null);\n };\n\n const closeCreate = () => {\n setShowCreate(false);\n resetForm();\n };\n\n const handleCreate = (e: React.FormEvent) => {\n e.preventDefault();\n setFormError(null);\n if (!name.trim() || !triggerEvent.trim()) {\n setFormError(tr('workflows.form.nameRequired', 'Name and trigger event are required.'));\n return;\n }\n const input: SubCreateWorkflowInput = {\n name: name.trim(),\n triggerEvent: triggerEvent.trim(),\n ...(description.trim() ? { description: description.trim() } : {}),\n };\n create.mutate(input, {\n onSuccess: () => {\n resetForm();\n setShowCreate(false);\n },\n onError: (err) =>\n setFormError(err instanceof Error ? err.message : tr('common.error', 'An error occurred')),\n });\n };\n\n return (\n <PageLayout\n title={tr('workflows.title', 'Workflows')}\n description={tr('workflows.description', 'Event-driven automation across your portfolio')}\n actions={\n <button\n type=\"button\"\n onClick={() => setShowCreate(true)}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n <Plus className=\"h-4 w-4\" /> {tr('workflows.newWorkflow', 'New workflow')}\n </button>\n }\n >\n <PagePurpose className=\"mb-4\">\n {tr(\n 'workflows.purpose',\n \"Workflows react to domain events — a new invoice, a detected renewal, a cancelled relationship — and run multi-step automations. Draft a workflow, then activate it when you're ready.\"\n )}\n </PagePurpose>\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <select\n aria-label={tr('common.filter', 'Filter')}\n value={statusFilter}\n onChange={(e) => setStatusFilter(e.target.value as '' | SubWorkflowStatus)}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {STATUS_OPTIONS.map((o) => (\n <option key={o.value} value={o.value}>\n {tr(o.labelKey, o.fallback)}\n </option>\n ))}\n </select>\n </div>\n\n {isLoading ? (\n <LoadingView label={tr('workflows.loading', 'Loading workflows…')} />\n ) : isError ? (\n <ErrorView\n message={tr('workflows.error', 'Could not load workflows.')}\n onRetry={() => void refetch()}\n />\n ) : workflows.length === 0 ? (\n <div className=\"mt-6\">\n <EmptyState\n illustration=\"empty-data\"\n icon={<WorkflowIcon className=\"h-12 w-12\" />}\n title={tr('workflows.empty.title', 'No workflows yet')}\n description={tr(\n 'workflows.empty.description',\n 'Create your first event-driven workflow to automate your recurring portfolio.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => setShowCreate(true)}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n {tr('workflows.newWorkflow', 'New workflow')}\n </button>\n }\n />\n </div>\n ) : (\n <>\n {/* Overview (computed) */}\n <div className=\"mt-6 grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <MetricsCard\n icon={<WorkflowIcon className=\"h-5 w-5\" />}\n value={workflows.length}\n label={tr('workflows.metrics.workflows', 'Workflows')}\n />\n <MetricsCard\n icon={<Zap className=\"h-5 w-5\" />}\n value={activeCount}\n label={tr('workflows.metrics.active', 'Active')}\n />\n <MetricsCard\n icon={<Play className=\"h-5 w-5\" />}\n value={totalRuns}\n label={tr('workflows.metrics.totalRuns', 'Total runs')}\n />\n </div>\n\n {/* Selected workflow flow */}\n {selected && (\n <WorkflowFlowPanel\n workflow={selected}\n onClose={() => setSelectedId(null)}\n onActivate={() => activate.mutate(selected.id)}\n activating={activate.isPending}\n />\n )}\n\n {/* Workflow cards */}\n <div className=\"mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {workflows.map((wf) => {\n const isSelected = wf.id === selectedId;\n return (\n <GlassCard\n key={wf.id}\n treatment=\"metric\"\n glow={isSelected || wf.status === 'ACTIVE'}\n role=\"button\"\n tabIndex={0}\n aria-pressed={isSelected}\n aria-label={tr('workflows.card.viewAriaLabel', 'View flow for {{name}}', {\n name: wf.name,\n })}\n onClick={() => setSelectedId(isSelected ? null : wf.id)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setSelectedId(isSelected ? null : wf.id);\n }\n }}\n className=\"flex cursor-pointer flex-col p-5 focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"flex size-10 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-text-primary\">\n <WorkflowIcon className=\"h-5 w-5\" />\n </span>\n <StatusBadge status={wf.status} />\n </div>\n <p className=\"mt-3 truncate text-sm font-semibold text-text-primary\">{wf.name}</p>\n <p className=\"flex items-center gap-1 truncate text-xs text-text-secondary\">\n <Zap className=\"h-3 w-3 shrink-0\" aria-hidden />{' '}\n {tr('workflows.card.onTrigger', 'on {{trigger}}', { trigger: wf.triggerEvent })}\n </p>\n {wf.description && (\n <p className=\"mt-2 line-clamp-2 min-h-[2rem] text-xs text-text-secondary\">\n {wf.description}\n </p>\n )}\n <p className=\"mt-3 flex items-center gap-1.5 text-xs text-text-secondary tabular-nums\">\n <Play className=\"h-3.5 w-3.5\" aria-hidden />{' '}\n {tr('workflows.flow.ran', 'Ran {{count}}×', { count: wf.runCount })}\n {wf.lastRunAt\n ? ` · ${tr('workflows.flow.lastRun', 'last {{when}}', { when: formatRelativeTime(wf.lastRunAt) })}`\n : ''}\n </p>\n {wf.status !== 'ACTIVE' && (\n <button\n type=\"button\"\n disabled={activate.isPending}\n onClick={(e) => {\n e.stopPropagation();\n activate.mutate(wf.id);\n }}\n className=\"mt-4 inline-flex items-center justify-center gap-1.5 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 <Play className=\"h-3.5 w-3.5\" /> {tr('common.activate', 'Activate')}\n </button>\n )}\n </GlassCard>\n );\n })}\n </div>\n </>\n )}\n\n {/* Create modal */}\n {showCreate && (\n <Modal title={tr('workflows.newWorkflow', 'New workflow')} onClose={closeCreate}>\n <form onSubmit={handleCreate} 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=\"wf-name\" className={labelCls}>\n {tr('workflows.form.nameLabel', 'Name')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"wf-name\"\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder={tr('workflows.form.namePlaceholder', 'e.g. Invoice → notify finance')}\n className={inputCls}\n required\n />\n </div>\n <div>\n <label htmlFor=\"wf-trigger\" className={labelCls}>\n {tr('workflows.form.triggerLabel', 'Trigger event')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"wf-trigger\"\n type=\"text\"\n value={triggerEvent}\n onChange={(e) => setTriggerEvent(e.target.value)}\n placeholder={tr(\n 'workflows.form.triggerPlaceholder',\n 'e.g. InvoiceReceived, RenewalDetected'\n )}\n className={inputCls}\n required\n />\n </div>\n <div>\n <label htmlFor=\"wf-desc\" className={labelCls}>\n {tr('workflows.form.descriptionLabel', 'Description')}\n </label>\n <textarea\n id=\"wf-desc\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n rows={3}\n placeholder={tr(\n 'workflows.form.descriptionPlaceholder',\n 'What should this workflow do when the event fires?'\n )}\n className={inputCls}\n />\n </div>\n <div className=\"flex justify-end gap-3\">\n <button\n type=\"button\"\n onClick={closeCreate}\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('common.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('workflows.creating', 'Creating…')\n : tr('workflows.createWorkflow', 'Create workflow')}\n </button>\n </div>\n </form>\n </Modal>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"WorkflowsPage.js","names":[],"sources":["../../../src/subscriberbot/pages/WorkflowsPage.tsx"],"sourcesContent":["/**\n * Workflows (Automation)\n * @module subscriberbot/pages\n *\n * The Workflow Engine — event-driven automation (\"when invoice arrives →\n * classify → update relationship → notify finance\"). List + create + activate.\n * Reads `subListWorkflows`; writes `subCreateWorkflow` / `subActivateWorkflow`.\n *\n * Opening a workflow renders a READ-ONLY horizontal step-flow derived from the\n * workflow's real `triggerEvent`, `name` and `status` (the svc contract does\n * not expose an editable step array — this is a truthful visualisation, not a\n * builder).\n */\n\nimport { useState } from 'react';\nimport {\n Plus,\n Workflow as WorkflowIcon,\n Play,\n Zap,\n Sparkles,\n CheckCircle2,\n ArrowRight,\n Clock,\n X,\n} from 'lucide-react';\nimport { PagePurpose, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n EmptyState,\n Modal,\n MetricsCard,\n LoadingView,\n ErrorView,\n} from '../components';\nimport { useWorkflows, useCreateWorkflow, useActivateWorkflow } from '../hooks/useSubscriberBotApi';\nimport { useTr } from '../hooks/useTr';\nimport { useRouteSelection } from '../hooks/useRouteSelection';\nimport { formatRelativeTime } from '../utils/formatters';\nimport type { SubCreateWorkflowInput, SubWorkflow, SubWorkflowStatus } from '../types';\n\nconst STATUS_OPTIONS: Array<{ value: '' | SubWorkflowStatus; labelKey: string; fallback: string }> =\n [\n { value: '', labelKey: 'workflows.filters.allStatuses', fallback: 'All statuses' },\n { value: 'ACTIVE', labelKey: 'workflows.filters.active', fallback: 'Active' },\n { value: 'DRAFT', labelKey: 'workflows.filters.draft', fallback: 'Draft' },\n { value: 'PAUSED', labelKey: 'workflows.filters.paused', fallback: 'Paused' },\n { value: 'ARCHIVED', labelKey: 'workflows.filters.archived', fallback: 'Archived' },\n ];\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// ---------------------------------------------------------------------------\n// A single node in the read-only step flow.\n// ---------------------------------------------------------------------------\nfunction FlowNode({\n icon,\n kicker,\n title,\n sub,\n accent,\n}: {\n icon: React.ReactNode;\n kicker: string;\n title: string;\n sub?: string;\n /** Top accent bar tone. */\n accent: 'trigger' | 'run' | 'done';\n}) {\n const bar =\n accent === 'done'\n ? 'bg-status-success-text'\n : accent === 'run'\n ? 'bg-status-warning-text'\n : 'bg-[var(--color-accent-indigo)]';\n return (\n <div className=\"w-48 shrink-0 overflow-hidden rounded-xl border border-border-subtle bg-bg-surface shadow-sm\">\n <div className={`h-1 w-full ${bar}`} aria-hidden />\n <div className=\"p-3\">\n <span className=\"flex size-8 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n {icon}\n </span>\n <p className=\"mt-2 text-[10px] font-medium uppercase tracking-wide text-text-secondary\">\n {kicker}\n </p>\n <p className=\"truncate text-sm font-semibold text-text-primary\" title={title}>\n {title}\n </p>\n {sub && <p className=\"mt-0.5 truncate text-xs text-text-secondary\">{sub}</p>}\n </div>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Read-only flow panel for the selected workflow.\n// ---------------------------------------------------------------------------\nfunction WorkflowFlowPanel({\n workflow,\n onClose,\n onActivate,\n activating,\n}: {\n workflow: SubWorkflow;\n onClose: () => void;\n onActivate: () => void;\n activating: boolean;\n}) {\n const tr = useTr();\n const connector = (\n <div className=\"flex shrink-0 items-center px-1 text-text-secondary\" aria-hidden>\n <span className=\"h-px w-4 bg-border-seam sm:w-6\" />\n <ArrowRight className=\"h-4 w-4\" />\n </div>\n );\n\n const statusLabel = workflow.status.charAt(0) + workflow.status.slice(1).toLowerCase();\n\n return (\n <GlassCard treatment=\"glass\" glow className=\"mt-6 p-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <p className=\"text-base font-semibold text-text-primary\">{workflow.name}</p>\n <StatusBadge status={workflow.status} />\n </div>\n <p className=\"mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-text-secondary\">\n <span className=\"tabular-nums\">\n {tr('workflows.flow.ran', 'Ran {{count}}×', { count: workflow.runCount })}\n </span>\n {workflow.lastRunAt && (\n <span className=\"inline-flex items-center gap-1\">\n <Clock className=\"h-3 w-3\" aria-hidden />{' '}\n {tr('workflows.flow.lastRun', 'last {{when}}', {\n when: formatRelativeTime(workflow.lastRunAt),\n })}\n </span>\n )}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label={tr('workflows.flow.closeAriaLabel', 'Close flow')}\n className=\"rounded-lg border border-border-subtle p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n\n {workflow.description && (\n <p className=\"mt-2 max-w-3xl text-sm text-text-secondary\">{workflow.description}</p>\n )}\n\n {/* Horizontal read-only step flow (scrolls on small screens) */}\n <div className=\"mt-4 overflow-x-auto pb-2\">\n <div className=\"flex min-w-max items-stretch\">\n <FlowNode\n accent=\"trigger\"\n kicker={tr('workflows.flow.trigger', 'When')}\n icon={<Zap className=\"h-4 w-4\" />}\n title={workflow.triggerEvent}\n sub={tr('workflows.flow.triggerEvent', 'Trigger event')}\n />\n {connector}\n <FlowNode\n accent=\"run\"\n kicker={tr('workflows.flow.run', 'Run')}\n icon={<Sparkles className=\"h-4 w-4\" />}\n title={workflow.name}\n sub={tr('workflows.flow.automation', 'Automation')}\n />\n {connector}\n <FlowNode\n accent=\"done\"\n kicker={tr('workflows.flow.then', 'Then')}\n icon={<CheckCircle2 className=\"h-4 w-4\" />}\n title={\n workflow.status === 'ACTIVE'\n ? tr('workflows.flow.live', 'Live')\n : tr('workflows.flow.awaitingActivation', 'Awaiting activation')\n }\n sub={tr('workflows.flow.status', 'Status: {{status}}', { status: statusLabel })}\n />\n </div>\n </div>\n\n {workflow.status !== 'ACTIVE' && (\n <div className=\"mt-3 flex justify-end\">\n <button\n type=\"button\"\n disabled={activating}\n onClick={onActivate}\n className=\"inline-flex items-center gap-1.5 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 <Play className=\"h-4 w-4\" />{' '}\n {activating\n ? tr('common.activating', 'Activating…')\n : tr('workflows.flow.activate', 'Activate workflow')}\n </button>\n </div>\n )}\n </GlassCard>\n );\n}\n\nexport function WorkflowsPage() {\n const [statusFilter, setStatusFilter] = useState<'' | SubWorkflowStatus>('');\n const [showCreate, setShowCreate] = useState(false);\n // The selected workflow lives in the URL (/workflows/:selectedId) so it can be\n // deep-linked and the assistant can resolve \"this workflow\".\n const [selectedId, setSelectedId] = useRouteSelection('workflows');\n const tr = useTr();\n\n const { data, isLoading, isError, refetch } = useWorkflows(statusFilter || undefined);\n const create = useCreateWorkflow();\n const activate = useActivateWorkflow();\n\n const [name, setName] = useState('');\n const [triggerEvent, setTriggerEvent] = useState('');\n const [description, setDescription] = useState('');\n const [formError, setFormError] = useState<string | null>(null);\n\n const workflows = data ?? [];\n const selected = workflows.find((w) => w.id === selectedId) ?? null;\n\n // --- Real, computed aggregates -------------------------------------------\n const activeCount = workflows.filter((w) => w.status === 'ACTIVE').length;\n const totalRuns = workflows.reduce((s, w) => s + (w.runCount || 0), 0);\n\n const resetForm = () => {\n setName('');\n setTriggerEvent('');\n setDescription('');\n setFormError(null);\n };\n\n const closeCreate = () => {\n setShowCreate(false);\n resetForm();\n };\n\n const handleCreate = (e: React.FormEvent) => {\n e.preventDefault();\n setFormError(null);\n if (!name.trim() || !triggerEvent.trim()) {\n setFormError(tr('workflows.form.nameRequired', 'Name and trigger event are required.'));\n return;\n }\n const input: SubCreateWorkflowInput = {\n name: name.trim(),\n triggerEvent: triggerEvent.trim(),\n ...(description.trim() ? { description: description.trim() } : {}),\n };\n create.mutate(input, {\n onSuccess: () => {\n resetForm();\n setShowCreate(false);\n },\n onError: (err) =>\n setFormError(err instanceof Error ? err.message : tr('common.error', 'An error occurred')),\n });\n };\n\n return (\n <PageLayout\n title={tr('workflows.title', 'Workflows')}\n description={tr('workflows.description', 'Event-driven automation across your portfolio')}\n actions={\n <button\n type=\"button\"\n onClick={() => setShowCreate(true)}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n <Plus className=\"h-4 w-4\" /> {tr('workflows.newWorkflow', 'New workflow')}\n </button>\n }\n >\n <PagePurpose className=\"mb-4\">\n {tr(\n 'workflows.purpose',\n \"Workflows react to domain events — a new invoice, a detected renewal, a cancelled relationship — and run multi-step automations. Draft a workflow, then activate it when you're ready.\"\n )}\n </PagePurpose>\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <select\n aria-label={tr('common.filter', 'Filter')}\n value={statusFilter}\n onChange={(e) => setStatusFilter(e.target.value as '' | SubWorkflowStatus)}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {STATUS_OPTIONS.map((o) => (\n <option key={o.value} value={o.value}>\n {tr(o.labelKey, o.fallback)}\n </option>\n ))}\n </select>\n </div>\n\n {isLoading ? (\n <LoadingView label={tr('workflows.loading', 'Loading workflows…')} />\n ) : isError ? (\n <ErrorView\n message={tr('workflows.error', 'Could not load workflows.')}\n onRetry={() => void refetch()}\n />\n ) : workflows.length === 0 ? (\n <div className=\"mt-6\">\n <EmptyState\n illustration=\"empty-data\"\n icon={<WorkflowIcon className=\"h-12 w-12\" />}\n title={tr('workflows.empty.title', 'No workflows yet')}\n description={tr(\n 'workflows.empty.description',\n 'Create your first event-driven workflow to automate your recurring portfolio.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => setShowCreate(true)}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n {tr('workflows.newWorkflow', 'New workflow')}\n </button>\n }\n />\n </div>\n ) : (\n <>\n {/* Overview (computed) */}\n <div className=\"mt-6 grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <MetricsCard\n icon={<WorkflowIcon className=\"h-5 w-5\" />}\n value={workflows.length}\n label={tr('workflows.metrics.workflows', 'Workflows')}\n />\n <MetricsCard\n icon={<Zap className=\"h-5 w-5\" />}\n value={activeCount}\n label={tr('workflows.metrics.active', 'Active')}\n />\n <MetricsCard\n icon={<Play className=\"h-5 w-5\" />}\n value={totalRuns}\n label={tr('workflows.metrics.totalRuns', 'Total runs')}\n />\n </div>\n\n {/* Selected workflow flow */}\n {selected && (\n <WorkflowFlowPanel\n workflow={selected}\n onClose={() => setSelectedId(null)}\n onActivate={() => activate.mutate(selected.id)}\n activating={activate.isPending}\n />\n )}\n\n {/* Workflow cards */}\n <div className=\"mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {workflows.map((wf) => {\n const isSelected = wf.id === selectedId;\n return (\n <GlassCard\n key={wf.id}\n treatment=\"metric\"\n glow={isSelected || wf.status === 'ACTIVE'}\n role=\"button\"\n tabIndex={0}\n aria-pressed={isSelected}\n aria-label={tr('workflows.card.viewAriaLabel', 'View flow for {{name}}', {\n name: wf.name,\n })}\n onClick={() => setSelectedId(isSelected ? null : wf.id)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setSelectedId(isSelected ? null : wf.id);\n }\n }}\n className=\"flex cursor-pointer flex-col p-5 focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"flex size-10 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-text-primary\">\n <WorkflowIcon className=\"h-5 w-5\" />\n </span>\n <StatusBadge status={wf.status} />\n </div>\n <p className=\"mt-3 truncate text-sm font-semibold text-text-primary\">{wf.name}</p>\n <p className=\"flex items-center gap-1 truncate text-xs text-text-secondary\">\n <Zap className=\"h-3 w-3 shrink-0\" aria-hidden />{' '}\n {tr('workflows.card.onTrigger', 'on {{trigger}}', { trigger: wf.triggerEvent })}\n </p>\n {wf.description && (\n <p className=\"mt-2 line-clamp-2 min-h-[2rem] text-xs text-text-secondary\">\n {wf.description}\n </p>\n )}\n <p className=\"mt-3 flex items-center gap-1.5 text-xs text-text-secondary tabular-nums\">\n <Play className=\"h-3.5 w-3.5\" aria-hidden />{' '}\n {tr('workflows.flow.ran', 'Ran {{count}}×', { count: wf.runCount })}\n {wf.lastRunAt\n ? ` · ${tr('workflows.flow.lastRun', 'last {{when}}', { when: formatRelativeTime(wf.lastRunAt) })}`\n : ''}\n </p>\n {wf.status !== 'ACTIVE' && (\n <button\n type=\"button\"\n disabled={activate.isPending}\n onClick={(e) => {\n e.stopPropagation();\n activate.mutate(wf.id);\n }}\n className=\"mt-4 inline-flex items-center justify-center gap-1.5 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 <Play className=\"h-3.5 w-3.5\" /> {tr('common.activate', 'Activate')}\n </button>\n )}\n </GlassCard>\n );\n })}\n </div>\n </>\n )}\n\n {/* Create modal */}\n {showCreate && (\n <Modal title={tr('workflows.newWorkflow', 'New workflow')} onClose={closeCreate}>\n <form onSubmit={handleCreate} 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=\"wf-name\" className={labelCls}>\n {tr('workflows.form.nameLabel', 'Name')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"wf-name\"\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder={tr('workflows.form.namePlaceholder', 'e.g. Invoice → notify finance')}\n className={inputCls}\n required\n />\n </div>\n <div>\n <label htmlFor=\"wf-trigger\" className={labelCls}>\n {tr('workflows.form.triggerLabel', 'Trigger event')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"wf-trigger\"\n type=\"text\"\n value={triggerEvent}\n onChange={(e) => setTriggerEvent(e.target.value)}\n placeholder={tr(\n 'workflows.form.triggerPlaceholder',\n 'e.g. InvoiceReceived, RenewalDetected'\n )}\n className={inputCls}\n required\n />\n </div>\n <div>\n <label htmlFor=\"wf-desc\" className={labelCls}>\n {tr('workflows.form.descriptionLabel', 'Description')}\n </label>\n <textarea\n id=\"wf-desc\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n rows={3}\n placeholder={tr(\n 'workflows.form.descriptionPlaceholder',\n 'What should this workflow do when the event fires?'\n )}\n className={inputCls}\n />\n </div>\n <div className=\"flex justify-end gap-3\">\n <button\n type=\"button\"\n onClick={closeCreate}\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('common.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('workflows.creating', 'Creating…')\n : tr('workflows.createWorkflow', 'Create workflow')}\n </button>\n </div>\n </form>\n </Modal>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1,21 +1,53 @@
1
- interface ConversationContextShape {
2
- entityType?: string;
1
+ /**
2
+ * Page context for the Subscriber Bot assistant.
3
+ *
4
+ * `gatherPageContext()` runs before every assistant turn and its result is sent
5
+ * to the assistant sandbox, which keeps only the keys in the agent's
6
+ * `ALLOWED_CONTEXT_KEYS` (ai-assistant `src/context-loader.ts`). Nothing filters
7
+ * on this side, so every key returned here crosses the wire: return only
8
+ * allow-listed keys, and never a message body, a payment-instrument detail or
9
+ * anything else that identifies a person. `userAgent` and `timestamp` are
10
+ * deliberately absent for the same reason (AI readiness criteria 3.1 and 3.3).
11
+ *
12
+ * `entityType` + `entityId` are what let the agent act on "this" in one turn
13
+ * ("unsubscribe from this", "pause this", "cancel this"), so every page that has
14
+ * an addressable entity in its URL must resolve here.
15
+ */
16
+ /**
17
+ * The keys the assistant agent forwards to the model. Kept in step with
18
+ * `ALLOWED_CONTEXT_KEYS` in ai-assistant `src/context-loader.ts`; the unit test
19
+ * asserts that `gatherPageContext()` never returns a key outside this list.
20
+ */
21
+ export declare const ASSISTANT_ALLOWED_CONTEXT_KEYS: readonly ["currentUrl", "currentPage", "entityType", "entityId", "entityName", "pageTitle", "pageType", "activeTab", "activeTool", "selectedText", "workspaceId", "workspaceName", "projectId", "projectName", "platform", "version", "viewportSize"];
22
+ export type AssistantContextKey = (typeof ASSISTANT_ALLOWED_CONTEXT_KEYS)[number];
23
+ /** Entity types the Subscriber Bot routes resolve to. The agent guide documents each one. */
24
+ export declare const SUBSCRIBERBOT_ENTITY_TYPES: {
25
+ readonly relationship: "SUBSCRIBERBOT_RELATIONSHIP";
26
+ readonly provider: "SUBSCRIBERBOT_PROVIDER";
27
+ readonly agent: "SUBSCRIBERBOT_AGENT";
28
+ readonly inboxMessage: "SUBSCRIBERBOT_INBOX_MESSAGE";
29
+ readonly workflow: "SUBSCRIBERBOT_WORKFLOW";
30
+ };
31
+ export type SubscriberBotEntityType = (typeof SUBSCRIBERBOT_ENTITY_TYPES)[keyof typeof SUBSCRIBERBOT_ENTITY_TYPES];
32
+ export interface ConversationContextShape {
33
+ entityType?: SubscriberBotEntityType;
3
34
  entityId?: string;
4
35
  activeTab?: string;
5
- metadata?: Record<string, unknown>;
6
36
  }
7
- export interface PageContext {
37
+ export type PageContext = Partial<Record<AssistantContextKey, string>> & {
8
38
  currentUrl: string;
9
39
  currentPage: string;
10
- entityType?: string;
11
- entityId?: string;
12
40
  platform: string;
13
41
  version: string;
14
42
  viewportSize: string;
15
- userAgent: string;
16
- timestamp: string;
17
- }
18
- export declare function gatherPageContext(): PageContext;
43
+ };
44
+ /**
45
+ * Resolve the entity the user is looking at from the router location.
46
+ *
47
+ * `pathname` is the full browser path; the Subscriber Bot routes live under
48
+ * `/subscriberbot`. Anything that is not an entity route resolves to an empty
49
+ * entity, so the agent asks rather than acting on a guessed id.
50
+ */
19
51
  export declare function resolveSubscriberBotConversationContext(pathname: string, search: string): ConversationContextShape;
20
- export {};
52
+ export declare function gatherPageContext(): PageContext;
21
53
  //# sourceMappingURL=pageContext.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pageContext.d.ts","sourceRoot":"","sources":["../../../src/subscriberbot/utils/pageContext.ts"],"names":[],"mappings":"AAAA,UAAU,wBAAwB;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,iBAAiB,IAAI,WAAW,CAoB/C;AAMD,wBAAgB,uCAAuC,CACrD,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,wBAAwB,CAoC1B"}
1
+ {"version":3,"file":"pageContext.d.ts","sourceRoot":"","sources":["../../../src/subscriberbot/utils/pageContext.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,uPAkBjC,CAAC;AAEX,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,8BAA8B,CAAC,CAAC,MAAM,CAAC,CAAC;AAElF,6FAA6F;AAC7F,eAAO,MAAM,0BAA0B;;;;;;CAM7B,CAAC;AAEX,MAAM,MAAM,uBAAuB,GACjC,CAAC,OAAO,0BAA0B,CAAC,CAAC,MAAM,OAAO,0BAA0B,CAAC,CAAC;AAE/E,MAAM,WAAW,wBAAwB;IACvC,UAAU,CAAC,EAAE,uBAAuB,CAAC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC,GAAG;IACvE,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAgCF;;;;;;GAMG;AACH,wBAAgB,uCAAuC,CACrD,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,wBAAwB,CAkB1B;AAED,wBAAgB,iBAAiB,IAAI,WAAW,CAgB/C"}
@@ -1,47 +1,52 @@
1
1
  //#region src/subscriberbot/utils/pageContext.ts
2
- function e() {
3
- let e = window.location.href, t = window.location.pathname.split("/").filter(Boolean).slice(0, 3).join("/") || "home", r = n(window.location.pathname, window.location.search);
4
- return {
5
- currentUrl: e,
6
- currentPage: t,
7
- entityType: r.entityType,
8
- entityId: r.entityId,
9
- platform: "subscriberbot",
10
- version: "2026.615.1",
11
- viewportSize: `${window.innerWidth}x${window.innerHeight}`,
12
- userAgent: navigator.userAgent,
13
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
14
- };
15
- }
16
- function t(e, t) {
17
- return t.match(e);
2
+ var e = {
3
+ relationship: "SUBSCRIBERBOT_RELATIONSHIP",
4
+ provider: "SUBSCRIBERBOT_PROVIDER",
5
+ agent: "SUBSCRIBERBOT_AGENT",
6
+ inboxMessage: "SUBSCRIBERBOT_INBOX_MESSAGE",
7
+ workflow: "SUBSCRIBERBOT_WORKFLOW"
8
+ }, t = [
9
+ ["relationships", e.relationship],
10
+ ["providers", e.provider],
11
+ ["agents", e.agent],
12
+ ["inbox", e.inboxMessage],
13
+ ["workflows", e.workflow]
14
+ ], n = new Set(["new"]), r = /^[A-Za-z0-9_-]{1,128}$/, i = /^[A-Za-z0-9_-]{1,64}$/;
15
+ function a(e) {
16
+ try {
17
+ return decodeURIComponent(e);
18
+ } catch {
19
+ return;
20
+ }
18
21
  }
19
- function n(e, n) {
20
- let r = new URLSearchParams(n);
21
- for (let [n, r] of [
22
- [/^\/subscriberbot\/relationships\/([^/]+)$/, "SUBSCRIBERBOT_RELATIONSHIP"],
23
- [/^\/subscriberbot\/providers\/([^/]+)$/, "SUBSCRIBERBOT_PROVIDER"],
24
- [/^\/subscriberbot\/agents\/([^/]+)$/, "SUBSCRIBERBOT_AGENT"],
25
- [/^\/subscriberbot\/invoices\/([^/]+)$/, "SUBSCRIBERBOT_INVOICE"],
26
- [/^\/subscriberbot\/workflows\/([^/]+)$/, "SUBSCRIBERBOT_WORKFLOW"]
27
- ]) {
28
- let i = t(n, e);
29
- if (i) return {
30
- entityType: r,
31
- entityId: i[1],
32
- metadata: { routePattern: n.source }
22
+ function o(e, o) {
23
+ let s = e.split("/").filter(Boolean), c = s.indexOf("subscriberbot"), l = c === -1 ? [] : s.slice(c + 1), u = new URLSearchParams(o).get("tab"), d = u && i.test(u) ? u : void 0;
24
+ if (l.length === 2) {
25
+ let [e, i] = l, o = t.find(([t]) => t === e), s = a(i);
26
+ if (o && s && !n.has(s) && r.test(s)) return {
27
+ entityType: o[1],
28
+ entityId: s,
29
+ ...d ? { activeTab: d } : {}
33
30
  };
34
31
  }
35
- let i = r.get("tab") ?? (() => {
36
- let t = e.split("/").filter(Boolean);
37
- return t.length > 2 ? t[t.length - 1] : void 0;
38
- })();
32
+ return d ? { activeTab: d } : {};
33
+ }
34
+ function s() {
35
+ let { href: e, pathname: t, search: n } = window.location, r = t.split("/").filter(Boolean).slice(0, 3).join("/") || "home", i = o(t, n);
39
36
  return {
40
- activeTab: i ?? void 0,
41
- metadata: { ...i ? { activeTab: i } : {} }
37
+ currentUrl: e,
38
+ currentPage: r,
39
+ ...i.entityType && i.entityId ? {
40
+ entityType: i.entityType,
41
+ entityId: i.entityId
42
+ } : {},
43
+ ...i.activeTab ? { activeTab: i.activeTab } : {},
44
+ platform: "subscriberbot",
45
+ version: "2026.615.1",
46
+ viewportSize: `${window.innerWidth}x${window.innerHeight}`
42
47
  };
43
48
  }
44
49
  //#endregion
45
- export { e as gatherPageContext };
50
+ export { s as gatherPageContext };
46
51
 
47
52
  //# sourceMappingURL=pageContext.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pageContext.js","names":[],"sources":["../../../src/subscriberbot/utils/pageContext.ts"],"sourcesContent":["interface ConversationContextShape {\n entityType?: string;\n entityId?: string;\n activeTab?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface PageContext {\n currentUrl: string;\n currentPage: string;\n entityType?: string;\n entityId?: string;\n platform: string;\n version: string;\n viewportSize: string;\n userAgent: string;\n timestamp: string;\n}\n\nexport function gatherPageContext(): PageContext {\n const url = window.location.href;\n const pathSegments = window.location.pathname.split('/').filter(Boolean);\n const currentPage = pathSegments.slice(0, 3).join('/') || 'home';\n const entity = resolveSubscriberBotConversationContext(\n window.location.pathname,\n window.location.search\n );\n\n return {\n currentUrl: url,\n currentPage,\n entityType: entity.entityType,\n entityId: entity.entityId,\n platform: 'subscriberbot',\n version: import.meta.env.VITE_APP_VERSION ?? '2026.615.1',\n viewportSize: `${window.innerWidth}x${window.innerHeight}`,\n userAgent: navigator.userAgent,\n timestamp: new Date().toISOString(),\n };\n}\n\nfunction matchPath(pattern: RegExp, pathname: string): RegExpMatchArray | null {\n return pathname.match(pattern);\n}\n\nexport function resolveSubscriberBotConversationContext(\n pathname: string,\n search: string\n): ConversationContextShape {\n const params = new URLSearchParams(search);\n const directMatches: Array<[RegExp, string]> = [\n [/^\\/subscriberbot\\/relationships\\/([^/]+)$/, 'SUBSCRIBERBOT_RELATIONSHIP'],\n [/^\\/subscriberbot\\/providers\\/([^/]+)$/, 'SUBSCRIBERBOT_PROVIDER'],\n [/^\\/subscriberbot\\/agents\\/([^/]+)$/, 'SUBSCRIBERBOT_AGENT'],\n [/^\\/subscriberbot\\/invoices\\/([^/]+)$/, 'SUBSCRIBERBOT_INVOICE'],\n [/^\\/subscriberbot\\/workflows\\/([^/]+)$/, 'SUBSCRIBERBOT_WORKFLOW'],\n ];\n\n for (const [pattern, entityType] of directMatches) {\n const match = matchPath(pattern, pathname);\n if (match) {\n return {\n entityType,\n entityId: match[1],\n metadata: {\n routePattern: pattern.source,\n },\n };\n }\n }\n\n const activeTab =\n params.get('tab') ??\n (() => {\n const segments = pathname.split('/').filter(Boolean);\n return segments.length > 2 ? segments[segments.length - 1] : undefined;\n })();\n\n return {\n activeTab: activeTab ?? undefined,\n metadata: {\n ...(activeTab ? { activeTab } : {}),\n },\n };\n}\n"],"mappings":";AAmBA,SAAgB,IAAiC;CAC/C,IAAM,IAAM,OAAO,SAAS,MAEtB,IADe,OAAO,SAAS,SAAS,MAAM,GAAG,EAAE,OAAO,OAC5C,EAAa,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,QACpD,IAAS,EACb,OAAO,SAAS,UAChB,OAAO,SAAS,MAClB;CAEA,OAAO;EACL,YAAY;EACZ;EACA,YAAY,EAAO;EACnB,UAAU,EAAO;EACjB,UAAU;EACV,SAA6C;EAC7C,cAAc,GAAG,OAAO,WAAW,GAAG,OAAO;EAC7C,WAAW,UAAU;EACrB,4BAAW,IAAI,KAAK,GAAE,YAAY;CACpC;AACF;AAEA,SAAS,EAAU,GAAiB,GAA2C;CAC7E,OAAO,EAAS,MAAM,CAAO;AAC/B;AAEA,SAAgB,EACd,GACA,GAC0B;CAC1B,IAAM,IAAS,IAAI,gBAAgB,CAAM;CASzC,KAAK,IAAM,CAAC,GAAS,MAAe;EAPlC,CAAC,6CAA6C,4BAA4B;EAC1E,CAAC,yCAAyC,wBAAwB;EAClE,CAAC,sCAAsC,qBAAqB;EAC5D,CAAC,wCAAwC,uBAAuB;EAChE,CAAC,yCAAyC,wBAAwB;CAGhC,GAAe;EACjD,IAAM,IAAQ,EAAU,GAAS,CAAQ;EACzC,IAAI,GACF,OAAO;GACL;GACA,UAAU,EAAM;GAChB,UAAU,EACR,cAAc,EAAQ,OACxB;EACF;CAEJ;CAEA,IAAM,IACJ,EAAO,IAAI,KAAK,YACT;EACL,IAAM,IAAW,EAAS,MAAM,GAAG,EAAE,OAAO,OAAO;EACnD,OAAO,EAAS,SAAS,IAAI,EAAS,EAAS,SAAS,KAAK,KAAA;CAC/D,GAAG;CAEL,OAAO;EACL,WAAW,KAAa,KAAA;EACxB,UAAU,EACR,GAAI,IAAY,EAAE,aAAU,IAAI,CAAC,EACnC;CACF;AACF"}
1
+ {"version":3,"file":"pageContext.js","names":[],"sources":["../../../src/subscriberbot/utils/pageContext.ts"],"sourcesContent":["/**\n * Page context for the Subscriber Bot assistant.\n *\n * `gatherPageContext()` runs before every assistant turn and its result is sent\n * to the assistant sandbox, which keeps only the keys in the agent's\n * `ALLOWED_CONTEXT_KEYS` (ai-assistant `src/context-loader.ts`). Nothing filters\n * on this side, so every key returned here crosses the wire: return only\n * allow-listed keys, and never a message body, a payment-instrument detail or\n * anything else that identifies a person. `userAgent` and `timestamp` are\n * deliberately absent for the same reason (AI readiness criteria 3.1 and 3.3).\n *\n * `entityType` + `entityId` are what let the agent act on \"this\" in one turn\n * (\"unsubscribe from this\", \"pause this\", \"cancel this\"), so every page that has\n * an addressable entity in its URL must resolve here.\n */\n\n/**\n * The keys the assistant agent forwards to the model. Kept in step with\n * `ALLOWED_CONTEXT_KEYS` in ai-assistant `src/context-loader.ts`; the unit test\n * asserts that `gatherPageContext()` never returns a key outside this list.\n */\nexport const ASSISTANT_ALLOWED_CONTEXT_KEYS = [\n 'currentUrl',\n 'currentPage',\n 'entityType',\n 'entityId',\n 'entityName',\n 'pageTitle',\n 'pageType',\n 'activeTab',\n 'activeTool',\n 'selectedText',\n 'workspaceId',\n 'workspaceName',\n 'projectId',\n 'projectName',\n 'platform',\n 'version',\n 'viewportSize',\n] as const;\n\nexport type AssistantContextKey = (typeof ASSISTANT_ALLOWED_CONTEXT_KEYS)[number];\n\n/** Entity types the Subscriber Bot routes resolve to. The agent guide documents each one. */\nexport const SUBSCRIBERBOT_ENTITY_TYPES = {\n relationship: 'SUBSCRIBERBOT_RELATIONSHIP',\n provider: 'SUBSCRIBERBOT_PROVIDER',\n agent: 'SUBSCRIBERBOT_AGENT',\n inboxMessage: 'SUBSCRIBERBOT_INBOX_MESSAGE',\n workflow: 'SUBSCRIBERBOT_WORKFLOW',\n} as const;\n\nexport type SubscriberBotEntityType =\n (typeof SUBSCRIBERBOT_ENTITY_TYPES)[keyof typeof SUBSCRIBERBOT_ENTITY_TYPES];\n\nexport interface ConversationContextShape {\n entityType?: SubscriberBotEntityType;\n entityId?: string;\n activeTab?: string;\n}\n\nexport type PageContext = Partial<Record<AssistantContextKey, string>> & {\n currentUrl: string;\n currentPage: string;\n platform: string;\n version: string;\n viewportSize: string;\n};\n\n/**\n * Route segment → entity type, for every route that carries one entity id.\n * Each entry mirrors a route in `SubscriberBotRoutes.tsx`; a pattern for a\n * route the app does not define would never match a real page.\n */\nconst ENTITY_ROUTES: ReadonlyArray<readonly [segment: string, type: SubscriberBotEntityType]> = [\n ['relationships', SUBSCRIBERBOT_ENTITY_TYPES.relationship],\n ['providers', SUBSCRIBERBOT_ENTITY_TYPES.provider],\n ['agents', SUBSCRIBERBOT_ENTITY_TYPES.agent],\n ['inbox', SUBSCRIBERBOT_ENTITY_TYPES.inboxMessage],\n ['workflows', SUBSCRIBERBOT_ENTITY_TYPES.workflow],\n];\n\n/** Static sub-routes that sit where an id would (\"relationships/new\" is a form, not an entity). */\nconst NON_ENTITY_SEGMENTS: ReadonlySet<string> = new Set(['new']);\n\n/** Ids are opaque, but they are path segments: no separators, no whitespace, bounded length. */\nconst ENTITY_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;\n\n/** Tab names are short identifiers; anything else is ignored rather than forwarded. */\nconst TAB_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;\n\nfunction decodeSegment(segment: string): string | undefined {\n try {\n return decodeURIComponent(segment);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Resolve the entity the user is looking at from the router location.\n *\n * `pathname` is the full browser path; the Subscriber Bot routes live under\n * `/subscriberbot`. Anything that is not an entity route resolves to an empty\n * entity, so the agent asks rather than acting on a guessed id.\n */\nexport function resolveSubscriberBotConversationContext(\n pathname: string,\n search: string\n): ConversationContextShape {\n const segments = pathname.split('/').filter(Boolean);\n const root = segments.indexOf('subscriberbot');\n const rest = root === -1 ? [] : segments.slice(root + 1);\n\n const tabParam = new URLSearchParams(search).get('tab');\n const activeTab = tabParam && TAB_PATTERN.test(tabParam) ? tabParam : undefined;\n\n if (rest.length === 2) {\n const [section, rawId] = rest as [string, string];\n const route = ENTITY_ROUTES.find(([segment]) => segment === section);\n const id = decodeSegment(rawId);\n if (route && id && !NON_ENTITY_SEGMENTS.has(id) && ENTITY_ID_PATTERN.test(id)) {\n return { entityType: route[1], entityId: id, ...(activeTab ? { activeTab } : {}) };\n }\n }\n\n return activeTab ? { activeTab } : {};\n}\n\nexport function gatherPageContext(): PageContext {\n const { href, pathname, search } = window.location;\n const currentPage = pathname.split('/').filter(Boolean).slice(0, 3).join('/') || 'home';\n const entity = resolveSubscriberBotConversationContext(pathname, search);\n\n return {\n currentUrl: href,\n currentPage,\n ...(entity.entityType && entity.entityId\n ? { entityType: entity.entityType, entityId: entity.entityId }\n : {}),\n ...(entity.activeTab ? { activeTab: entity.activeTab } : {}),\n platform: 'subscriberbot',\n version: import.meta.env.VITE_APP_VERSION ?? '2026.615.1',\n viewportSize: `${window.innerWidth}x${window.innerHeight}`,\n };\n}\n"],"mappings":";AA4CA,IAAa,IAA6B;CACxC,cAAc;CACd,UAAU;CACV,OAAO;CACP,cAAc;CACd,UAAU;AACZ,GAwBM,IAA0F;CAC9F,CAAC,iBAAiB,EAA2B,YAAY;CACzD,CAAC,aAAa,EAA2B,QAAQ;CACjD,CAAC,UAAU,EAA2B,KAAK;CAC3C,CAAC,SAAS,EAA2B,YAAY;CACjD,CAAC,aAAa,EAA2B,QAAQ;AACnD,GAGM,IAA2C,IAAI,IAAI,CAAC,KAAK,CAAC,GAG1D,IAAoB,0BAGpB,IAAc;AAEpB,SAAS,EAAc,GAAqC;CAC1D,IAAI;EACF,OAAO,mBAAmB,CAAO;CACnC,QAAQ;EACN;CACF;AACF;AASA,SAAgB,EACd,GACA,GAC0B;CAC1B,IAAM,IAAW,EAAS,MAAM,GAAG,EAAE,OAAO,OAAO,GAC7C,IAAO,EAAS,QAAQ,eAAe,GACvC,IAAO,MAAS,KAAK,CAAC,IAAI,EAAS,MAAM,IAAO,CAAC,GAEjD,IAAW,IAAI,gBAAgB,CAAM,EAAE,IAAI,KAAK,GAChD,IAAY,KAAY,EAAY,KAAK,CAAQ,IAAI,IAAW,KAAA;CAEtE,IAAI,EAAK,WAAW,GAAG;EACrB,IAAM,CAAC,GAAS,KAAS,GACnB,IAAQ,EAAc,MAAM,CAAC,OAAa,MAAY,CAAO,GAC7D,IAAK,EAAc,CAAK;EAC9B,IAAI,KAAS,KAAM,CAAC,EAAoB,IAAI,CAAE,KAAK,EAAkB,KAAK,CAAE,GAC1E,OAAO;GAAE,YAAY,EAAM;GAAI,UAAU;GAAI,GAAI,IAAY,EAAE,aAAU,IAAI,CAAC;EAAG;CAErF;CAEA,OAAO,IAAY,EAAE,aAAU,IAAI,CAAC;AACtC;AAEA,SAAgB,IAAiC;CAC/C,IAAM,EAAE,SAAM,aAAU,cAAW,OAAO,UACpC,IAAc,EAAS,MAAM,GAAG,EAAE,OAAO,OAAO,EAAE,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,QAC3E,IAAS,EAAwC,GAAU,CAAM;CAEvE,OAAO;EACL,YAAY;EACZ;EACA,GAAI,EAAO,cAAc,EAAO,WAC5B;GAAE,YAAY,EAAO;GAAY,UAAU,EAAO;EAAS,IAC3D,CAAC;EACL,GAAI,EAAO,YAAY,EAAE,WAAW,EAAO,UAAU,IAAI,CAAC;EAC1D,UAAU;EACV,SAA6C;EAC7C,cAAc,GAAG,OAAO,WAAW,GAAG,OAAO;CAC/C;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-subscriberbot",
3
- "version": "2026.1029.1",
3
+ "version": "2026.1030.1",
4
4
  "description": "Burdenoff SubscriberBot Microfrontend",
5
5
  "type": "module",
6
6
  "files": [
@@ -34,7 +34,8 @@
34
34
  "lint:sanity": "if [ \"${CI:-}\" = \"true\" ]; then bun run lint; else bun run lint -- --cache --cache-location .eslintcache; fi",
35
35
  "format": "prettier --write \"src/**/*.{ts,tsx}\"",
36
36
  "type:check": "bash scripts/build-limits.sh tsc --noEmit",
37
- "sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run lint:sanity && bun run format && bun run type:check && bun run build && bun run build-storybook'",
37
+ "test": "bun test src",
38
+ "sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run lint:sanity && bun run format && bun run type:check && bun run test && bun run build && bun run build-storybook'",
38
39
  "storybook": "storybook dev -p 6006",
39
40
  "build-storybook": "storybook build",
40
41
  "test:storybook": "test-storybook",
@@ -74,6 +75,7 @@
74
75
  "@storybook/test": "^8.6.0",
75
76
  "@storybook/test-runner": "0.22.0",
76
77
  "@tailwindcss/vite": "^4.3.2",
78
+ "@types/bun": "1.3.14",
77
79
  "@types/react": "^19.0.2",
78
80
  "@types/react-dom": "^19.0.2",
79
81
  "@vitejs/plugin-react": "^4.3.4",