The ledger
Every @feature annotation across the org, in one searchable index. Filter by repo, category, or maturity; each row resolves to the exact source line it was declared on.
Spectrum · top 12 categories
click to filter⬡ status coverage63%1,968 / 3,109 features annotated1,141 missing status:
worst offenders
- 11%openalice-embed601 missing
- 66%openalice-platform144 missing
- 78%openalice-persona66 missing
- 92%openalice60 missing
- 31%openalice-world36 missing
- 23%openalice-social34 missing
- 0%openalice-lab25 missing
- 0%openalice-inspector23 missing
0 matched0 in ledger
- embed-app.page.abtestsopenalice-embed · embed-app/pageA/B experiment management screen. Loads the widget from openalice-tenants (GET /me/widgets/{id}) by the `?id=` query param to resolve its public_id, then loads the widget's experiments (GET /me/experiments?widget_id=) and hands both to the client ABTestsView, which manages lifecycle actions and fetches per-variant results from the /api/experiments BFF proxy. Server component so the tenant token stays server-side. Renders clear states when no id is supplied, the dashboard isn't configured, or the widget isn't found — mirroring the analytics screen.
- embed-app.page.analyticsopenalice-embed · embed-app/pageConversion analytics — real aggregated KPIs, real by_day sparkline, real funnel (conversations → engaged ≥2 turns → escalations), a REAL account-level intent breakdown, and REAL revenue attribution wired to the conversion data. Date-range toggle (7/30/90) uses ?days= searchParam and works server-side so links are shareable and the full page re-fetches real data on change. Real data: - KPI stat row: conversations, messages, escalation count, cost, deflection rate - Sparkline: by_day from getAccountAnalytics(days) - Funnel: conversations → escalations (2-stage; the "Engaged ≥2 turns" proxy was removed — its heuristic was unreliable and always showed ~0% drop) - Top intents: getAccountIntentInsights(days) — merged (intent, count) across widgets, count-descending. Empty until conversations are classified. - Revenue attribution: getAccountRevenueInsights(days) — converted count, total value, conversion rate. Empty until conversions are marked.
- embed-app.page.analyticsopenalice-embed · embed-app/pageConversion analytics — real aggregated KPIs, real by_day sparkline, real funnel (conversations → engaged ≥2 turns → escalations), a REAL account-level intent breakdown, and REAL revenue attribution wired to the conversion data. Date-range toggle (7/30/90) uses ?days= searchParam and works server-side so links are shareable and the full page re-fetches real data on change. Real data: - KPI stat row: conversations, messages, escalation count, cost, deflection rate - Sparkline: by_day from getAccountAnalytics(days) - Funnel: conversations → escalations (2-stage; the "Engaged ≥2 turns" proxy was removed — its heuristic was unreliable and always showed ~0% drop) - Top intents: getAccountIntentInsights(days) — merged (intent, count) across widgets, count-descending. Empty until conversations are classified. - Revenue attribution: getAccountRevenueInsights(days) — converted count, total value, conversion rate. Empty until conversions are marked.
- embed-app.page.billingopenalice-embed · embed-app/pageBilling & plan screen — real, data-driven. Fetches the tenant's current Embed plan + monthly message usage + the plan catalog from openalice-tenants (GET /me/billing) server-side, renders a usage meter and a catalog grid with Upgrade CTAs that POST /me/billing/checkout (via the checkoutAction server action) and redirect the browser to the hosted Mollie checkout URL. Mollie is the payment processor — NOT Stripe or Paddle (org rule). Graceful gating: billing is FEATURE-FLAGGED on the tenants side (MOLLIE_API_KEY). When billing_enabled is false the page still renders the current plan + catalog but shows a "billing coming soon" banner and disables the Upgrade CTAs — it never crashes. Not-signed-in and error states get their own clear branches. Balance card (prepaid): Fetches GET /me/billing/balance server-side — mirrors the getBilling() pattern. Shows €X.XX available, context-aware status line, per-unit rates, recent transaction ledger, and a Top-up CTA (disabled when Mollie is not configured — billing_enabled gate reused from the plan checkout path).
- embed-app.page.billing-returnopenalice-embed · embed-app/pagePost-checkout return page — where Mollie sends the customer back after they complete (or abandon) the hosted checkout. The redirect_url carries no authoritative status (Mollie only confirms via the server-side webhook), so this page re-reads GET /me/billing and reflects whatever the webhook has already applied: a promoted plan means "you're live", while a still-Free plan means "we're confirming your payment" (the webhook may land a moment later). Always offers a link back to billing.
- embed-app.page.brandopenalice-embed · embed-app/pagePer-widget Appearance editor screen. Loads the real widget from openalice-tenants (GET /me/widgets/{id}) by the `?id=` query param and hands it to the client AppearanceEditor, which saves appearance.* via the saveAppearanceAction server action (PATCH /me/widgets/{id}). Server component so the tenant token stays server-side. Mirrors the persona page error-state pattern.
- embed-app.page.business-hoursopenalice-embed · embed-app/pagePer-widget Business-Hours / offline editor screen (§business-hours, migration 0030). Loads the real widget from openalice-tenants (GET /me/widgets/{id}) by the `?id=` query param and hands it to the client BusinessHoursEditor, which saves the business_hours JSONB blob {enabled, timezone, offline_message, weekly} via saveBusinessHoursAction (PATCH /me/widgets/{id} draft). When a visitor opens the widget outside the configured open hours, agent-lite computes is_offline server-side and the widget shows the offline message + the (Slice-1) lead-capture form. Server component so the tenant token stays server-side. Mirrors the Mode page error-state pattern.
- embed-app.page.conversationsopenalice-embed · embed-app/pageConversations page — server-rendered shell + real two-pane client. Server component responsibilities: 1. Fetch widget list (listWidgets) — used for widget selector tabs. 2. Determine active widget from ?widget=<public_id> or default to first. 3. Fetch real aggregate analytics for the stat strip. 4. Fetch first page of conversations for the active widget (SSR). 5. Pass all data to ConversationsClient (client component). The ConversationsClient handles widget switching, load-more pagination, and transcript loading — all via the /api/conversations proxy routes. The proxy routes inject x-internal-secret server-side; the secret never reaches the browser.
- embed-app.page.conversations.clientopenalice-embed · embed-app/pageInteractive two-pane conversation browser. Receives initial data from the server component (widget list, first widget's conversation list). Handles: - Widget selector (left toolbar) - Conversation list with "Load more" pagination (next_cursor) - Transcript detail pane (fetched on row click) - Honest empty state when retention=0 or no traffic - Live operator takeover (EMBED_LIVE_TAKEOVER_V1): visibility-aware 3s transcript poll while a conversation is open (skipped for ENDED conversations), Take over / Release, operator composer, control_mode badges + "Needs human" filter - 15s list refresh poll (skipped while document.hidden) - Auto-scroll to newest message when user is near the bottom - 401 session-expired recovery banner - Mobile master-detail (conversation selected → transcript panel only, "← Back" returns to list; desktop two-pane unchanged)
- embed-app.page.conversations.clientopenalice-embed · embed-app/pageTests for the inbox-depth console additions (#773 wave 2) inside ConversationsClient: Search bar: - The no-content-search hint is shown. - Typing fires a debounced list fetch carrying ?q=<text>. - A search that returns nothing shows the "no results" empty state. Tag filtering: - A row renders its tags as clickable chips. - Clicking a row tag chip fetches the list filtered by ?tag=<tag> and shows the active tag-filter chip; Clear resets it. Tag editor (detail header): - Existing tags render with a remove ✕. - Adding a tag POSTs /tags and shows the new chip. - Removing a tag POSTs DELETE /tags/{tag} and drops the chip. Notes panel (detail): - Existing notes render (text + author). - Adding a note POSTs /notes {text} and optimistically prepends it. fetch() is mocked with a URL/method router. Real timers + userEvent are used (the search debounce fires within waitFor's polling window).
- embed-app.page.conversations.clientopenalice-embed · embed-app/pageTests for the live operator-takeover console (EMBED_LIVE_TAKEOVER_V1) inside ConversationsClient: ControlBadge: - List rows show "Human · <name>" only while control_mode=human (agent rows stay unlabelled). - Detail header shows the widget's agent name (widget.name, default "Lily") in agent mode, "Human · <name>" in human mode (defensive default when the transcript omits control_mode). Needs-human filter chip: - Narrows rows to control_mode=human OR intent=escalation. - Toggling off restores the full list. Take over / Release: - Agent mode shows "Take over"; clicking POSTs /takeover and flips the badge to the server-confirmed operator name. - Human mode shows "Release to {agentName}"; clicking POSTs /release. OperatorComposer: - Disabled with the "Take over to reply" hint while control_mode=agent (Send disabled too). - Enabled in human mode; sending POSTs /operator-message {text} and optimistically appends an operator bubble. Operator turns: - role="operator" messages render with the "<name> · team" speaker label. fetch() is mocked with a URL/method router; the 3s poll re-serves the same transcript fixture so tests stay deterministic.
- embed-app.page.conversations.clientopenalice-embed · embed-app/pageTests for the ConversationsClient component and its sub-components. Covers: SentimentChip: - Renders the correct icon and aria-label for positive / neutral / negative. ConvRow: - Renders the conversation preview text when present. - Shows message count with correct singular/plural form ("1 msg" vs "N msgs"). - Renders SentimentChip when sentiment is provided. - Does NOT render SentimentChip when sentiment is absent. - Renders the ConvertedBadge when converted=true (with and without value). - Shows a short ID truncated with "…". - Has role="option" and responds to keyboard Enter. durationStr helper (exercised via row render): - ongoing when endedAt is absent. - seconds: "45s". - minutes: "2m". EmptyListPane: - With retentionDays=0: mentions "turned off" in the text. - With retentionDays>0: shows the general "no conversations" message. ConversationsClient (full component): - Shows EmptyListPane when initialList is null (no conversations). - Shows conversation rows when initialList contains items. - Shows an error banner when initialError is provided. - Shows widget tabs only when there are multiple widgets. - Does NOT show widget tabs when there is only one widget. - Clicking a row calls fetch() for the transcript endpoint. NOTE: We import ConversationsClient sub-components by rendering the full client component with controlled props (the sub-components are not exported). For sub-component unit tests we access them via rendered output. fetch() is mocked globally to avoid network calls.
- embed-app.page.dashboard-homeopenalice-embed · embed-app/pageCustomer dashboard home — real aggregated stats + real widget cards + data-derived weekly-insight teaser. Data sources: - listWidgets() → real widget cards (name, mode, status) - getAccountAnalytics(30) → stat strip (conversations, deflection rate, escalations, cost); insight teaser derived from per-widget escalation counts. Honest empty-states: - Stat strip shows "—" + neutral delta when no data / not configured. - Widget grid shows an honest empty-state card when no widgets exist. - Insight teaser shows "Insights appear once you have traffic." when there is no data yet.
- embed-app.page.forgot-passwordopenalice-embed · embed-app/page"Forgot password?" screen — single cream card (no split layout, no AppShell), reusing the login page's module styles. The email form posts to forgotPasswordAction which relays to the tenants auth-proxy; the page then shows ONE success view regardless of whether the email exists (anti-enumeration — see actions.ts).
- embed-app.page.insightsopenalice-embed · embed-app/pageWeekly insight digest wired to real data: REAL: cost card (LLM spend, conversations, cost/chat from getAccountAnalytics(7)) REAL: volume + escalation cards (totals + by_day from getAccountAnalytics(30)) REAL: per-widget highlight (widget with highest escalation count) REAL: sentiment card (breakdown + avg_score from getSentimentInsights per widget, 30d) REAL: knowledge gaps (top unanswered questions fan-out across all widgets, 30d) REAL: top-rated conversation (highest CSAT in 30d, fan-out across widgets, tie-broken by length then recency; honest empty state when unrated) Every card is wired to real data with an honest empty state — no fake numbers, no coming-soon placeholders.
- embed-app.page.insightsopenalice-embed · embed-app/pageWeekly insight digest wired to real data: REAL: cost card (LLM spend, conversations, cost/chat from getAccountAnalytics(7)) REAL: volume + escalation cards (totals + by_day from getAccountAnalytics(30)) REAL: per-widget highlight (widget with highest escalation count) REAL: sentiment card (breakdown + avg_score from getSentimentInsights per widget, 30d) REAL: knowledge gaps (top unanswered questions fan-out across all widgets, 30d) REAL: top-rated conversation (highest CSAT in 30d, fan-out across widgets, tie-broken by length then recency; honest empty state when unrated) Every card is wired to real data with an honest empty state — no fake numbers, no coming-soon placeholders.
- embed-app.page.installopenalice-embed · embed-app/pagePer-widget install screen. Loads the real widget from openalice-tenants (GET /me/widgets/{id}) by the `?id=` query param, then hands the real public_id and default_mode to the client InstallClient component. Server component so the tenant token stays server-side. Mirrors the persona page error-state pattern.
- embed-app.page.knowledgeopenalice-embed · embed-app/pageKnowledge base configuration for a single widget. What is REAL today: - Loads the widget by `?id=` and shows the configured `brand_kb_url`. - "Ingest from URL" form PATCHes `brand_kb_url` via `saveKbUrlAction` (the field tenants actually indexes from). - Document upload + re-index against the tenant KB store. - Live KB metrics (indexed chunks, source documents, last indexed) from agent-lite `/internal/kb/stats` — account-scoped (the RAG store keys by tenant, not per-widget), with an honest empty state when nothing is indexed and the panel hidden when the stats endpoint is unavailable. Honest by construction: coverage % and retrieval latency are NOT shown because they aren't measured. The old page showed hardcoded numbers (7 sources, 2,847 chunks, 87% coverage, 73ms) and dead buttons that looked functional — all of that is removed. No theatre.
- embed-app.page.leadsopenalice-embed · embed-app/pagePer-widget leads screen. Server component: resolves the widget from openalice-tenants by the `?id=` query param, fetches captured leads from the agent-lite internal read surface (GET /internal/insights/leads?widget_id=…&days=30&limit=100), and hands them to the client LeadsView for rendering. The INTERNAL_SECRET never reaches the browser — listLeads() is server-only (via agentLite.ts + the `server-only` import there). Mirrors the webhooks screen's notice / error-state pattern exactly.
- embed-app.page.loginopenalice-embed · embed-app/pageStandalone auth screen — split brand panel (dark-sakura, left) + real email+password sign-in / create-account card (cream, right). No AppShell. Tabs switch between Sign in / Create account. Both forms post to server actions (login/actions.ts) which talk to openalice-tenants server-side and set the HttpOnly oa_dash_session cookie on success — the plaintext password never leaves the server boundary.
- embed-app.page.loginopenalice-embed · embed-app/pageTests for the login page's landing-handoff query params. Key behaviors: - /login (no params) → the Sign-in tab is active, signup email is empty. - /login?email=<addr>&mode=signup → the Create-account tab opens directly and the signup form's email input is pre-filled with <addr>. - /login?email=<addr> (no mode) → stays on Sign-in, but the signup email is still pre-filled for when the user switches tabs. NOTE: useSearchParams comes from the vitest next/navigation stub (a vi.fn), so each test injects its own query string. next/link renders as a plain <a> so the page mounts without the Next runtime.
- embed-app.page.modeopenalice-embed · embed-app/pagePer-widget Mode editor screen. Loads the real widget from openalice-tenants (GET /me/widgets/{id}) by the `?id=` query param and hands it to the client ModeEditor, which saves default_mode + call_mode via the saveModeAction server action (PATCH /me/widgets/{id}). Server component so the tenant token stays server-side. Mirrors the persona page error-state pattern.
- embed-app.page.onboardingopenalice-embed · embed-app/pageFull-screen character-creation wizard (60-90s). No AppShell — standalone viewport takeover with its own topbar + footer stripe. 5-step real state machine (useReducer) wired to createWidgetAction: Step 1 — Name → name, persona_script Step 2 — Mode → mode (sales / support / brand) Step 3 — Voice → voice on/off (default natural voice) + consent_us_stt/tts Step 4 — Knowledge → brand_kb_url Step 5 — Review → submit → createWidgetAction → /widgets/persona?id=<id> Error from the server action is surfaced on the Review step without leaving the wizard. On success the action calls redirect(), which Next.js handles via thrown NEXT_REDIRECT — caught by the framework, not our catch.
- embed-app.page.onboardingopenalice-embed · embed-app/pageFull-screen character-creation wizard (60-90s). No AppShell — standalone viewport takeover with its own topbar + footer stripe. 5-step real state machine (useReducer) wired to createWidgetAction: Step 1 — Name → name, persona_script Step 2 — Mode → mode (sales / support / brand) Step 3 — Voice → voice on/off (default natural voice) + consent_us_stt/tts Step 4 — Knowledge → brand_kb_url Step 5 — Review → submit → createWidgetAction → /widgets/persona?id=<id> Error from the server action is surfaced on the Review step without leaving the wizard. On success the action calls redirect(), which Next.js handles via thrown NEXT_REDIRECT — caught by the framework, not our catch.
- embed-app.page.overviewopenalice-embed · embed-app/pagePer-widget Overview HOME — the landing screen for a single widget. Replaces the old "Overview" tab that dumped the user back to the all-widgets list (losing widget context). Gives each widget a real home: - status (Live / Draft) + the primary next action (Test drive / embed), - an at-a-glance facts strip (mode / model / languages / knowledge), - the setup guide (FirstRunChecklist, shared heuristics in widgetSetup), - a "what to do" journey grid (Set up → Launch → Grow) generated from the canonical widgetTabs() source, so the home and the tab strip never drift. Server component: loads the real widget from openalice-tenants by `?id=`, so the tenant token stays server-side. Error / empty states reuse the existing widgetCfgA notice copy (already translated in all 6 locales). The home's own body copy is English for now (i18n is a follow-up).
- embed-app.page.overviewopenalice-embed · embed-app/pageLayout-only module — every colour / surface / type token comes from the global design system (globals.css). No new colours or shadows are introduced here so the home stays visually consistent with the editor pages.
- embed-app.page.personaopenalice-embed · embed-app/pagePer-widget editor screen. Loads the real widget from openalice-tenants (GET /me/widgets/{id}) by the `?id=` query param and hands it to the client PersonaEditor, which saves name / persona script / mode and the Memory block (memory_mode + retention_days) via the saveWidgetAction server action. Server component so the tenant token stays server-side. Renders clear states when no id is supplied, the dashboard isn't configured, or the widget isn't found.
- embed-app.page.previewopenalice-embed · embed-app/pageClient island for the preview page — owns the desktop/mobile toggle and renders the real widget metadata (name, public_id, appearance) from the server-loaded WidgetFull into the status chips. Honesty note: the browser frame renders a static layout mockup to show widget placement and chrome — it does NOT mount the real embed SDK. A prominent banner makes this clear and links to Test Drive for the live widget. Use TestDriveClient for a real, interactive session.
- embed-app.page.previewopenalice-embed · embed-app/pageLive WYSIWYG preview of the widget — browser-frame + faux customer pricing page + docked OpenAlice widget (avatar, chat bubble, compose bar). Loads the real widget by ?id= from openalice-tenants. Desktop / mobile viewport toggle is handled by PreviewClient. Share preview link is Pro+ gated (CTA only, no real share flow at this stage). Server component — token stays server-side.
- embed-app.page.reset-passwordopenalice-embed · embed-app/pageSet-a-new-password screen — the destination of the reset email's <dashboard-base>/reset-password?token=… link. Single cream card reusing the login page's module styles. The token rides a hidden input into resetPasswordAction; success links the user to /login to sign in with the new password. A missing token (page opened without the email link) shows a request-a-new-link view instead of a doomed form.