The Trinity Beast Infrastructure — Account Dashboards

Customer self-service portal — passwordless auth, universal panel access with invitations, billing, API keys, usage charts, webhooks, support, and admin impersonation.

Account: 211998422884 Region: us-east-2 (Ohio) Dashboard: https://api.cpmp-site.org/dashboard Updated: August 15, 2026

1. Overview

The Trinity Beast Account Dashboard is a customer-facing self-service portal served at https://api.cpmp-site.org/dashboard. It gives every account holder a single place to view their subscription status, API usage, giving history, webhook configuration, and billing — all authenticated via passwordless magic link.

The dashboard is a Single Page Application (SPA) served by the LPO server directly. There are no separate frontend assets, no CDN dependency, and no build step — the entire SPA is embedded in the Go binary as string-concatenated HTML/JS. All data is fetched from /dashboard/api/* JSON endpoints after the initial page load.

Diagram 1.1: Dashboard Architecture Overview
graph LR
  subgraph Browser["Browser (SPA)"]
    direction TB
    SHELL["/dashboard — HTML Shell"] --> STORE["localStorage\ncpmp_user: token · lang · roles\ncpmp_site: lang"]
    STORE --> LANG["_detectLang()\ncpmp_user.lang → cpmp_site.lang\n→ navigator.languages → en"]
    LANG --> RTL["Set dir=ltr / rtl\nrtl for ar · ur"]
    STORE --> CALLS["JSON API Calls"]
  end
  subgraph LPO["LPO Server (ECS)"]
    direction TB
    SPAH["SPAHandler\nserves shell"] --> SESS["RequireSession\nmiddleware"]
    SESS --> PANEL["Panel API\nhandlers"]
    PUBL["PublicLangHandler\nno auth required"]
  end
  subgraph Data["Data Layer"]
    AUR["Aurora\napi_keys · users\nusage_logs"]
    VAL["Valkey\nsessions · magic links\nlang:{code} · audit log"]
    STR["Stripe\nsubscriptions\ncharges"]
  end
  CALLS -->|"Authorization: Bearer"| SESS
  LANG -->|"GET /public/lang/{code}"| PUBL
  PUBL --> VAL
  PANEL --> AUR
  PANEL --> VAL
  PANEL --> STR
    

2. Account Types & Universal Access

Every account sees every panel. The dashboard does not hide features a customer has not bought. Navigation is identical for a first-time donor, a Partner, and an admin — the only exception is the two admin-only panels. What differs is panel content: a customer who owns a product sees their live data, and a customer who does not sees a short invitation to it.

This replaced an earlier design in which the sidebar was assembled from whatever relationships the account held, which meant customers could not see what they did not already have.

Every dashboard user is resolved into an Account — a unified identity keyed by email address. One email can hold any combination of relationships simultaneously (e.g. a person who both gives to CPMP and subscribes to the API).

Resolved Relationships

The server resolves each account into a set of typed relationships. These determine what a panel displays — never whether the panel is reachable. They also drive the roles slice used for endpoint authorization, which is still enforced server-side.

RelationshipRole GrantedSourceDescription
DonorFacetdonorStripeActive recurring giving subscription via CPMP give page
APIKeyFacetapi-subscriberAurora api_keysActive REST API subscription (free, pro, enterprise, unlimited, lifetime)
TranslationKeyFacettranslation-customerAurora api_keysTranslation Service key (service_type = 'translation' or is_translation = true)
WebhookFacetwebhook-associateAurora api_keys + webhook_subscriptionsActive webhook push subscription (starter, standard, professional, enterprise)
PartnerFacetpartnerAurora api_keysAWS PrivateLink partner account
AdminadminAurora application_parametersEmail matches admin_email parameter — grants the two admin panels + impersonation

Naming note for maintainers. The Go type names still carry the *Facet suffix from the original design (internal/handlers/dashboard/models.go, resolved in resolver.go), and are serialized to the browser as the top-level keys donor, api_key, webhook, partner, and is_admin. The names are historical. They no longer imply that the UI is faceted, and no code in the SPA layer references the term at all.

Multi-Product Account Model

The dashboard uses an email-based account model. One email address = one account. Every API key, webhook subscription, translation key, and donation associated with that email appears in a single unified dashboard view.

This gives customers full control over how they organize their products:

The API response includes a products array alongside the legacy single-key convenience fields. Each product entry carries a type field (lpo, translation, webhook, partner) and the relevant data for that product. This allows the SPA to render organized product cards regardless of how many products the account holds.

Diagram 2.1: Account Resolution
flowchart TD
  A["resolveAccount(email)"] --> B["Aurora: JOIN api_keys + users\nWHERE email = ? AND revoked = false"]
  B --> TX{"service_type = 'translation'\nOR is_translation = true?"}
  TX -->|Yes| TXF["TranslationKeyFacet\n→ TranslationKeys[]"]
  TX -->|No| C{tier?}
  C -->|"free/pro/enterprise\nunlimited/lifetime"| D["APIKeyFacet\n→ LPOKeys[]"]
  C -->|"partner"| E["PartnerFacet\n→ PartnerKeys[]"]
  C -->|"webhook_*"| F["WebhookFacet\n→ WebhookKeys[]\n+ webhook_subscriptions query"]
  A --> G["Stripe: customer search by email\n→ active subscriptions"]
  G --> H["DonorFacet\n→ Subscriptions[]"]
  A --> I["application_parameters\nWHERE key = 'admin_email'"]
  I -->|"email matches"| J["IsAdmin = true"]
  D & TXF & E & F & H & J --> K["deriveRoles()\n→ roles slice"]
  K --> L["Account + products[]\nreturned to SPA"]
    

2.1 Sidebar Composition

The sidebar is built by buildSidebar(acc) and is the same for every account. Seven sections are emitted unconditionally; an eighth appears only for admins. There are no tier checks, no ownership checks, and no Giving / Services tab split.

SectionItems (route keys)Shown To
— (unlabeled)Overview (overview)Everyone
AccountProfile (account), Billing (billing), Change Plan (change-plan), Support (support)Everyone
GivingOverview (giving-overview), Donation History (giving-history), Your Impact (giving-impact)Everyone
API AccessAPI Key (api-key), Usage (usage), Rate Limits (rate-limits), Reports (reports)Everyone
TranslationTranslate (translate), History (translate-history)Everyone
WebhookConfig (webhook), Delivery Log (webhook-log), Delivery Reports (webhook-reports)Everyone
PartnerConnection (connection)Everyone
AdminAnalytics (analytics), Person View (person-view)is_admin only

Two further route keys exist in the panel router but are not sidebar items, because they are reached programmatically from the Support panel: support-detail and support-detail-customer.

2.2 The Invitation Model

Because navigation no longer gates anything, each panel is responsible for its own empty state. When the account lacks the relationship a panel depends on, the panel renders the page title as normal and then a single centered invitation card in place of the data.

Every invitation card follows one shape: a large icon, one sentence describing the value of the product in plain language, and one button linking to the public marketing page for it. No pricing appears in the card — pricing lives on the destination page, which reads from tier_catalog.

PanelShown WhenInvitation CopyButton → Destination
giving-overviewNo donation historyYour giving journey starts here. Every dollar funds freedom from brick kiln slavery.Join Us → /give.html
api-keyNo LPO products and no API keyGet live cryptocurrency prices from 6 exchanges with one API call.Subscribe → /subscribe-listener.html
usageNo API key and no Partner keyYour API usage charts appear here once you start making requests.Subscribe → /subscribe-listener.html
rate-limitsNo API keyRate limit details appear when you have an active API subscription.View Plans → /subscribe-listener.html
webhookNo webhook subscriptionGet prices pushed directly to your endpoint — UDP or HTTPS, your choice.Webhook Plans → /webhook.html
webhook-logNo webhook subscriptionWebhook delivery history appears once your push service is active.Webhook Plans → /webhook.html
connectionNo Partner keyAWS PrivateLink access — sub-2ms latency, zero rate limits, direct container access.Apply → /partner-apply.html

Three panels behave differently and are worth knowing about explicitly:

3. Authentication — Magic Link Flow

The dashboard uses passwordless magic link authentication. No passwords are stored or transmitted. A time-limited, single-use token is emailed to the user via SES and consumed atomically on first use.

Diagram 3.1: Magic Link Authentication Flow
sequenceDiagram
  participant U as User (Browser)
  participant S as LPO Server
  participant V as Valkey
  participant A as Aurora
  participant E as SES (Email)

  U->>S: POST /dashboard/api/request-link {email}
  S->>V: Rate limit check (30/hr per email, 60/hr per IP)
  S->>A: emailHasAccount(email)?
  alt account exists
    S->>S: generateToken() → 32-byte random
    S->>V: SET magic:{sha256(token)} payload TTL=15min
    S->>E: Send magic link email (async goroutine)
  end
  S-->>U: 200 "If an account exists, a link was sent" (always)

  U->>S: GET /dashboard/authenticate?token=...
  S->>V: GETDEL magic:{sha256(token)} (atomic, single-use)
  alt token valid
    S->>A: resolveAccount(email) → relationships + roles
    S->>S: generateToken() → session token
    S->>V: SET session:{sha256(token)} Session TTL=24h
    S-->>U: HTML page → localStorage.setItem('cpmp_user', JSON) → redirect /dashboard
  else token expired or used
    S-->>U: HTML error page "Link expired or already used"
  end
    

3.1 Token Security

PropertyValue
Token entropy32 bytes (256 bits) cryptographically random via crypto/rand
StorageSHA-256 hash stored in Valkey — raw token never persisted server-side
Magic link TTL15 minutes
Single-use enforcementAtomic GETDEL — consumed on first use, cannot be replayed
Session TTL24 hours, sliding (extended on every authenticated request)
Rate limiting30 requests/hour per email, 60 requests/hour per IP
Email enumerationAlways returns 200 regardless of whether account exists
TransportAuthorization: Bearer <token> header — never in cookies, no CSRF needed

3.2 Magic Link Email

Sent from The Trinity Beast <No-Reply@CPMP-Site.org> via SES (us-east-2). Gmail-compatible HTML: bgcolor attributes on every <td>, solid hex colors only, no rgba(). Subject: Your Dashboard Login Link — CPMP.

4. Session Management

Sessions are stored in Valkey under session:{sha256(token)}. The SPA stores the raw token in cpmp_user.token (localStorage JSON object) and sends it as a Bearer token on every API call. The RequireSession middleware validates and slides the TTL on every request.

Valkey KeyTTLContents
session:{hash}24h slidingEmail, roles, created_at, last_seen, IP, user_agent, impersonated_by (if admin session)
magic:{hash}15 minEmail, requested_at, IP, user_agent
ratelimit:magic:email:{email}1 hourRequest counter (max 30)
ratelimit:magic:ip:{ip}1 hourRequest counter (max 60)
audit:dashboard:{email}90 daysSorted set of audit events (last 500), scored by timestamp ms

4.1 Client-Side Storage (localStorage)

The dashboard and website use two localStorage JSON objects to persist state across page loads. Both use underscore-delimited names to distinguish them from the legacy flat keys they replaced.

KeyLifetimeStructureWritten By
cpmp_userSession (cleared on logout)
{
  "token": "Bearer session token (256-bit)",
  "email": "user@example.com",
  "name":  "Display Name",
  "roles": ["api-subscriber", "donor"],
  "lang":  "en"
}
Magic link authenticate endpoint (/dashboard/authenticate)
cpmp_sitePermanent
{
  "lang": "en"
}
i18n.js v5 (language selector flag dropdown)

Key Responsibilities

Language Resolution

The entire dashboard interface is multi-lingual, and the language is resolved client-side before any panel renders. _detectLang() walks four sources in order and returns the first hit:

OrderSourceWhy It Ranks Here
1cpmp_user.langThe authenticated user's own stored preference. Most specific — it is tied to the account, not the device.
2cpmp_site.langThe site-wide flag-dropdown selection, shared with the main website. Survives logout, so a returning visitor keeps their choice before authenticating.
3navigator.languagesBrowser preference order, filtered to the 12 supported codes. Gives a sensible first-visit default with no stored state.
4enFinal fallback.

The resolved code drives two things. It sets text direction — ar and ur set dir="rtl" on both documentElement and body, and every other language sets ltr. And it fetches the string catalog from GET /public/lang/{code}, which reads the lang:{code} key from Valkey. That endpoint requires no authentication and carries no session middleware — it is the one server call the SPA makes without a Bearer token, because the login screen itself must be localized before a session exists. If the fetch fails, the SPA retries against /public/lang/en so the interface degrades to English rather than to empty labels.

This is why editing a language JSON file is not enough on its own — the endpoint serves from Valkey, so bash scripts/kcc.sh push-langs must run for a change to reach the dashboard.

Legacy Key Migration

Prior to i18n.js v5 (May 2026), the language preference was stored as a flat string in localStorage('cpmp-lang'). On first page load after the upgrade, i18n.js automatically reads the old flat key, migrates the value into cpmp_site.lang, and removes the old key. This migration is transparent to users — no language preference is lost.

Session Validation on Load

On every page load, GET /dashboard/api/session is called to confirm the token in cpmp_user.token is still valid before rendering the dashboard. If the token is expired or revoked (e.g., after email change), the SPA clears cpmp_user and redirects to the login screen.

5. Dashboard Panels

All panels are rendered client-side by the SPA JavaScript. The server serves a single HTML shell; panel content is built from the account data returned by GET /dashboard/api/account plus async data fetched per-panel as needed.

Diagram 5.1: Panel Rendering — Owned vs. Invited
graph TD
  A["Account Loaded"] --> B["buildSidebar(acc)\n7 sections, unconditional"]
  B --> ADM{"is_admin?"}
  ADM -->|yes| ADM2["+ Admin section\nanalytics · person-view"]
  ADM -->|no| NAV["Same nav as everyone"]
  ADM2 --> NAV

  NAV --> C["User selects a panel"]
  C --> D["buildPanelContent(acc)\nswitch on state.panel"]

  D --> E{"Does the account hold\nthe relationship\nthis panel needs?"}
  E -->|"Yes"| F["Render live data\n(async fetch per panel)"]
  E -->|"No"| G["Render invitation card\nicon + one-line value prop\n+ link to marketing page"]

  D --> H["reports · translate\nno ownership check —\nfull UI for everyone"]
    

5.1 Overview Panel

The default landing panel and the router's fallback for any unrecognized route. Shows a welcome message, an organized product summary, and quick action buttons. Content adapts to which products the account holds.

If the account has donation history, a giving bar shows the lifetime total with a link to the Giving panels.

Product Cards (Multi-Product Accounts)

When the account holds one or more products (from the products array), the overview renders a "Your Products" card with one row per product. Each product row displays:

  • Product icon — 📡 REST API, 🌐 Translation, 🔔 Webhook, 🤝 Partner
  • Product label and tier badge — e.g., "REST API" with a "Pro" badge
  • Quick stats — usage/quota for LPO, "Pay per job" for Translation, asset count + interval for Webhook, connection status for Partner
  • Action buttons — direct navigation to the relevant panel (Key, Usage, Translate, Config)

This design scales naturally: a customer with one product sees one card, a customer with four products sees four cards — all in one clean view without needing tabs or navigation to discover what they have.

Giving Stats

If the account has donation history, giving status and lifetime total tiles appear below the product cards — or as the primary content for accounts that hold no other product.

Quick Actions

Context-aware shortcut buttons: View API Key, Usage Stats, Translate, Webhook Config, See Your Impact, Manage Billing. Only buttons relevant to the account's products are shown. This is the one place in the dashboard where content is still filtered by ownership rather than accompanied by an invitation — the full set of features remains one click away in the sidebar.

When the account holds no products at all, the overview falls back to a set of legacy stat tiles instead of the product card list.

Data source: GET /dashboard/api/account (no additional API call needed — the products array and all resolved relationship data arrive in a single response).

5.2 Giving Panels

Three panels, in every account's sidebar. An account with no donation history sees an invitation to give on the Overview panel and an inline Make your first gift link on Donation History:

PanelRoute KeyContentsData Source
Giving Overviewgiving-overviewStatus, monthly gift amount, total given, next renewal date, Stripe portal button. Invitation card when there is no donation history./account (donor data)
Donation Historygiving-historyTable of last 12 months of Stripe charges — date, amount, receipt linkGET /dashboard/api/giving/history
Impactgiving-impactPhoto gallery of CPMP mission work — medical camps, freedom moments, wheelchairs, Bible distribution, provisions, trainingStatic (embedded in SPA)

The Donation History panel fetches the last 12 months of successful Stripe charges. If Stripe has older records, a note is shown directing the user to contact support for a full history.

5.3 Account & Billing Panels

PanelRoute KeyContentsData Source
ProfileaccountEmail (with change option), display name (editable), preferred language, API response language, status indicator format, member since date, roles/account
BillingbillingCurrent Subscription card (live from Stripe): plan name, amount/interval, cancel status with date, next payment estimate, payment method (card brand + last 4). Cancel warning banner with "Don't cancel" reactivation button. Stripe Customer Portal button, payment method on file indicator, billing information (synced from portal).GET /dashboard/api/subscription-details
POST /dashboard/api/billing/portal
POST /dashboard/api/subscription/reactivate
Change Planchange-planCurrent Subscription card (same as Billing — shows cancel state before switching). Available plans list (from tier_catalog) with Switch buttons. Proration info. Go Lifetime card with credit calculation. Free-tier upgrade redirects to Stripe Checkout.GET /dashboard/api/subscription-details
GET /dashboard/api/available-plans
POST /dashboard/api/change-plan
GET /dashboard/api/lifetime-quote

The Billing panel shows a Current Subscription card at the top — live from Stripe — displaying the plan name, price, cancel status (with red badge and service end date), next estimated payment, and payment method (card brand + last 4 digits). If the subscription is scheduled for cancellation, a prominent "Don't cancel subscription" button lets the customer reactivate directly without visiting the Stripe portal. The same card appears on the Change Plan panel so customers see their current state before making changes.

The billing portal resolves the Stripe customer ID in order: Aurora api_keys.stripe_customer_idStripe customer search by email. The portal session URL is returned and opened in a new tab. Return URL is https://cpmp-site.org/dashboard.

5.3.1 API Preferences

The Profile panel includes two API preference controls that affect how the Unified Messaging Envelope (UME) delivers responses to the customer's API key:

ControlFieldOptionsDefaultEffect
API Response Languageapi_lang12 supported languages (en, es, pt, fr, de, ru, hi, ur, it, ar, ja, zh)enSets the language of translatable message content in API responses (error descriptions, informational messages). Envelope metadata (status brackets, endpoint, region, cluster node) remains English always.
Status Indicatorsresponse_formattbc (emoji) or plain (text)tbcControls whether status indicators use emoji symbols (✅ 🛑 ⚠️) or plain text equivalents (OK, ERROR, WARN). Applies to both the status field and the error field prefix.

Both preferences are saved via POST /dashboard/api/profile with fields api_lang and response_format. The handler updates ALL non-revoked api_keys rows for the user — a single toggle applies across all keys the account holds.

Note: api_lang is separate from preferred_lang. The preferred_lang field controls the language of emails, newsletters, and support communications. The api_lang field controls only API response messages.

5.4 API Key & Usage Panels

In every account's sidebar. Accounts with no API key see invitation cards on API Key, Usage, and Rate Limits; the Reports panel renders its full UI regardless.

PanelRoute KeyContentsData Source
API Keyapi-keyMasked key display, Reveal button (fetches full key on demand), Copy button (appears after reveal), key details (tier, status, LRS, renewal)GET /dashboard/api/api-key/reveal (on Reveal click)
UsageusageCurrent month request count (from active key), quota progress bar (color-coded: green/amber/red), 30-day daily bar chart (consolidated across all user's API keys via user_id)GET /dashboard/api/usage
Rate Limitsrate-limitsQPS limit, burst limit, monthly quota. Shown for all tiers — on unlimited and lifetime the quota row reads Unlimited rather than a number./account (API key data)
LRS ReportsreportsInteractive reports panel with Usage and Summary tabs. Date range picker, asset filter, pagination (30/60/90 per page), and export (JSON/CSV/TSV/Text). Proxies to local LRS using the user's user_id — consolidates history across all API keys the user has ever held (tier changes, key rotations). Available to all API key holders regardless of tier — same monthly report limits apply.GET /dashboard/api/reports/usage
GET /dashboard/api/reports/summary

The API key is masked by default (lpo-****-**** style). The Reveal action calls /api-key/reveal, which requires api-subscriber, translation-customer, webhook-associate, partner, or admin role. For accounts with multiple keys, the reveal endpoint accepts an optional ?key_id= parameter to reveal a specific key. Every reveal is audit-logged.

The 30-day usage chart is rendered as proportional bars from the usage_logs table, grouped by day in EST timezone.

5.5 Webhook Panels

In every account's sidebar — three panels. Accounts with no webhook subscription see an invitation card on all three.

PanelRoute KeyContentsStatus
Webhook ConfigurationwebhookCurrent plan, asset count, push interval, HTTPS endpoint, UDP endpoint, last delivery timestamp. Categorized asset picker with a real 24h-volume sort toggle and name tooltips, plus an endpoint configuration wizard. A pending subscription shows an "Activate Delivery" step before it can go live (see 5.5.2).Live — full management
Delivery Logwebhook-logRecent delivery history, grouped by real push event with a 3-minute activity rollup (see 5.5.3)Live
Delivery Reportswebhook-reportsDaily/summary reporting on delivery rate, asset coverage, and latency, with date-range filtering and 4-format export (see 5.5.4)Live

The asset picker draws from the shared exchange_asset_map catalog (150+ prewarmed assets across 6 exchanges — see Trinity-Beast-Kiro-Command-Center.html). A market-volatility disclaimer appears above the picker on both the Webhook and Partner panels, noting that thinly-traded picks can go stale and are swappable at any time. Plan-tier asset limits are enforced server-side at subscription creation: Starter 9 assets/60s, Standard 30/15s, Professional 60/6s (resized from 75 on 2026-08-10 — see 5.5.1 note below), Enterprise 60/3s (resized from 150 on 2026-08-18). Professional and Enterprise deliberately share the same asset ceiling and differ only in push interval.

Note on advertised vs. real-world delivery.

The webhook engine's own price cache resolves a roughly liquidity-bound number of fresh assets per push rather than a fixed fraction of the configured tier size. After engineering fixes on 2026-08-08 (a Valkey fallback tier and a widened webhook-specific freshness window), typical delivery is around 47–70% of the configured asset count at the higher tiers, and improves further as the prewarm pool matures. Professional's advertised count was lowered from 75 to 60 assets on 2026-08-10 to bring the promise closer to what the exchange pool reliably supports. Enterprise was resolved on 2026-08-18: rather than advertise an asset count the market cannot supply, the tier was moved to the same 60-asset ceiling as Professional at $420/mo, differentiated solely by its 3-second push interval — exactly twice Professional's rate for exactly twice the price. The reasoning is a liquidity fact: of our 150+ prewarmed assets, only about 65 trade above $1M per 24 hours, so any ceiling far above that necessarily reaches into markets too thin to supply a fresh price at a 3-second cadence. Selling the interval instead of the count makes the promise keepable, and a fast interval is only meaningful on liquid assets in the first place.

5.5.1 Asset Picker — Volume Sort & Full Catalog

The asset picker offers two sort modes, switchable without a re-fetch: A–Z (alphabetical) and Most Traded (ranked by real 24-hour USD trading volume, pulled from each exchange's own ticker feed via bash scripts/kcc.sh refresh-volume and stored on exchange_asset_map.volume_24h_usd). Hovering an asset chip shows its full display name (e.g. hovering BTC shows "Bitcoin") from the display_name column, backfilled from CoinGecko.

By default the category browser caps each of the 7 groups at 9 assets (ranked by 30-day request volume, then alphabetically) to keep the picker compact. Passing ?full=1 to the categories endpoint returns every enabled asset in the category, uncapped — this is what the webhook and partner asset pickers use so a Professional or Enterprise subscriber can see the complete pool, not just the top 9 per category.

5.5.2 Activating a Pending Subscription

A new webhook subscription is created in pending_verification status. The Configuration panel shows a "2. Activate Delivery" card with a delivery-method dropdown (UDP + HTTPS, UDP only, or HTTPS only) and the matching endpoint fields — a UDP host/port pair and/or an HTTPS URL. Submitting calls POST /dashboard/api/webhook/activate, which validates the IP and port range, requires the assets step to already be complete, and flips the row to active with verified_at set. This intentionally skips the public API's token echo-back verification step — a logged-in dashboard session already proves control of the account, so the extra round-trip that a machine-to-machine signup needs is not required here.

5.5.2b Tier Downgrade — Over-Limit Sticky Banner

When a subscriber downgrades their tier (e.g. Professional → Standard) and their configured asset count exceeds the new tier's limit, the Webhook Configuration panel shows a sticky amber banner at the top of the asset wizard area:

"Your current configuration has X assets, which exceeds the Y-asset limit of your [Tier] plan. Please remove assets to match your new limit."

Behavior:

  • Delivery continues — the existing asset selection keeps pushing at the new tier's interval. No auto-truncation, no silent asset drops.
  • The banner is sticky (position:sticky; top:0) — it stays visible while scrolling through the chip grid, so the subscriber cannot miss it.
  • An "Action Required" email is also sent the first time the over-limit state is detected after a downgrade.
  • The subscriber must reconfigure — remove assets via the chip-picker or PUT /webhook/assets to bring the selection within the new tier's cap. The banner disappears once the count is at or below the limit.

This is a graceful grandfather: delivery is never interrupted by a tier change. The banner and email are informational prompts, not enforcement gates.

5.5.3 Delivery Log — Recent Pushes & Activity Rollup

The Delivery Log panel reads directly from webhook_delivery_log, capped at the most recent 50 push events — it is a recent-activity view, not a second LRS Reports. For history beyond that window, or for billing-grade totals, use Delivery Reports (5.5.4) or LRS Reports.

Rows are grouped server-side by (sequence_number, delivery_method) — one row per real push, not per asset — showing time (America/New_York, to the second, since Enterprise's 3-second interval can otherwise collapse dozens of distinct pushes into one visible minute), delivery method, status (with attempt count if a push needed a retry), latency, and asset count. Clicking a row expands it to show every asset and price included in that specific push. Every column header is clickable to sort, and clicking again reverses the sort — this all happens client-side against the already-fetched 50-event array, with no re-fetch.

Below the push table, an Activity Rollup card buckets the same 50-event snapshot into fixed 3-minute windows — start/end time, push count, delivered/failed counts, and average latency per window. The bucket size is fixed and stated once above the table (rather than per row), and the push count is always shown next to the average latency specifically so a 2-push window and a 60-push window never read the same.

5.5.4 Delivery Reports — Daily & Summary

Built 2026-08-11 after a customer question about whether webhook reporting reads from the same place as LRS Reports (it doesn't — LRS reads Valkey-cached usage logs; webhook reporting reads Aurora directly). Delivery Reports gives a subscriber date-ranged, aggregate answers to "how well is my subscription actually performing" without scanning the raw per-asset log live.

Backed by a permanent Aurora rollup table, webhook_delivery_daily, refreshed automatically every 15 minutes by a pg_cron job that rescans only the last 2 days of raw log (not a full-history rescan). Each row aggregates one subscription's one day on one delivery method: push counts, success/failure counts, asset-row counts, distinct assets delivered, the day's configured asset-list size (snapshotted, not joined live — so historical coverage percentages stay meaningful after a later tier or asset-list change), and average/p95 latency.

The panel offers a date range picker, summary cards (delivery rate, asset coverage, average latency — deliberately phrased in plain terms rather than audit language, while keeping the exact numbers), a daily breakdown table, and all 4 export formats (JSON/CSV/TSV/Text) — the same convention as LRS Reports. A "Refresh Now" button forces an immediate rollup update outside the 15-minute cron cadence, rate-limited to one call per subscription per 60 seconds; the manual refresh scans a 3-day window rather than the full retention period, avoiding the ALB idle-timeout 502 that a full 93-day rescan produced during testing.

Ownership of the webhook subscription is the only gate on this panel — it is deliberately not tied to LRS entitlement, since Webhook Push and LRS Reports are separate products.

MethodPathDescription
GET/dashboard/api/webhook/reports/dailyDaily breakdown rows for the caller's subscription, date-range filterable, 4-format export
GET/dashboard/api/webhook/reports/summaryAggregate summary cards over the selected date range
POST/dashboard/api/webhook/reports/refreshStarts an on-demand rollup refresh asynchronously — returns 202 immediately (3-day scan window, 60s per-subscription cooldown)
GET/dashboard/api/webhook/reports/refresh/statusPoll for the outcome of a refresh started above

A public-API mirror exists at GET /webhook/reports/daily, /webhook/reports/summary, POST /webhook/reports/refresh, and GET /webhook/reports/refresh/status — same behavior, authenticated by API key rather than dashboard session.

5.6 Partner Connection Panel

In every account's sidebar. Accounts without a Partner key see an invitation card linking to the Partner application page (cpmp-site.org/partner-apply.html). Partners also see a Reports link in the sidebar under the Partner section — this routes directly to the LRS Reports panel (§5.4), which partners access free and unlimited by tier policy.

5.6.1 PrivateLink Status Card

FieldDescription
Connection Statusactive for any provisioned, non-revoked partner key. Color-coded: green (connected/active), amber (degraded), red (disconnected). Future: live TCP probe or CloudWatch health data once partners create VPC endpoints.
PrivateLink EndpointAWS PrivateLink endpoint identifier (or "—" if no consumer has connected yet)
SLA TierPartner SLA level (or "—" if not yet assigned)
Hourly VolumeRequests per hour (or "0 requests" for freshly onboarded partners)

5.6.2 Asset Watchlist Wizard

The same categorized chip-picker pattern as the Webhook asset wizard (§5.5.1), but with no asset cap — Partner tier has unlimited access to all assets by policy. The watchlist is purely organizational (analogous to the internal EAM tool curating which assets are actively tracked) and does not restrict what the Partner-tier key can query.

Features:

  • Chip grid grouped by asset category (Major Currencies, DeFi, Layer 2, etc.)
  • Toggle between A-Z sort and Most Traded sort
  • Asset name tooltips on hover (sourced from GET /asset-names)
  • Full catalog mode (?full=1) — shows all 360 prewarmed assets, not just top 9 per category
  • Market volatility disclaimer: "Thinly-traded assets may go stale between price updates. Swap any pick for a more actively-traded asset at any time."
  • Counter badge showing selected count (no maximum)

Data sources:

  • GET /dashboard/api/partner/config — current watchlist (api_key_id, tier, asset_count, assets array)
  • POST /dashboard/api/partner/assets — update the selection (body: {"assets":["BTC","ETH","SOL",...]}). Deduplicates and normalizes to uppercase. Empty array is valid ("no watchlist curated yet" — access unaffected).
  • GET /dashboard/api/partner/available-assets — the full pickable catalog (same source as the webhook asset picker — GET /asset-categories?full=1)

5.6.3 Partner Onboarding Flow (Approval → Dashboard)

When an admin approves a partner application, the system automatically:

  1. Provisions a real API key (Partner tier, unlimited access, LRS included free)
  2. Sends a welcome email with a 72-hour authenticated magic-link token embedded as /dashboard/authenticate?token=... — so the partner logs in as themselves immediately (not into a stale session)

The longer-lived token (72h vs the normal 15-minute magic link) is deliberate — partners may not open the welcome email immediately.

5.7 Support Panel (Customer)

Available to all authenticated accounts. Provides a full inline support experience — customers can view their tickets, read the reply thread, post replies, and mark tickets as resolved without leaving the dashboard.

5.7.1 Ticket List

Displays all support tickets associated with the authenticated user's email, ordered by most recent first. Each row shows ticket number, subject, category, status, and last updated date.

Data source: GET /dashboard/api/support/tickets

5.7.2 Ticket Detail & Thread

Clicking a ticket opens the full conversation thread. Shows the original message, all non-internal replies (admin internal notes are never visible to customers), and the current status. Replies from admin are displayed with translated content when the customer's preferred_lang is not English.

Data source: GET /dashboard/api/support/tickets/{ticket_number}

5.7.3 Customer Reply

Customers can post replies to open tickets directly from the dashboard. Replies are limited to 10,000 characters. If the customer's language is not English, the reply is auto-translated to English for admin readability (stored in message_translated). Posting a reply notifies the admin via email.

Status flow: A customer reply to a ticket in awaiting_customer or resolved status automatically re-opens it to open.

Data source: POST /dashboard/api/support/tickets/{ticket_number}/reply

5.7.4 Mark as Resolved

Customers can mark their own ticket as resolved from the dashboard. This sets the status to resolved and notifies the admin. Tickets already in resolved or closed status return a success message without changes.

Alternatively, customers can resolve tickets via a single-use email link (included in admin reply notifications). The link contains a 32-byte token stored in Valkey with a 30-day TTL, consumed atomically on use.

Data source: POST /dashboard/api/support/tickets/{ticket_number}/resolve

5.7.5 Ticket Statuses

StatusMeaning
newJust submitted, not yet reviewed by admin
openAdmin has replied or customer re-opened
in_progressAdmin is actively working on it
awaiting_customerAdmin is waiting for customer response
resolvedMarked resolved by customer or admin
closedPermanently closed — no further replies allowed from dashboard

5.7.6 Ticket Categories

CategoryDescription
generalGeneral inquiry
api-technicalAPI integration or technical issue
billingBilling, subscription, or payment question
bug-reportBug or unexpected behavior
feature-requestFeature suggestion
mission-donationsCPMP mission or donation inquiry

5.8 Admin Support Panel

Visible only when is_admin: true. Provides a full ticket management interface — view all tickets across all customers, filter by status/category, read threads (including internal notes), post replies, change status, and manage the support queue.

5.8.1 All Tickets View

Lists all support tickets system-wide, ordered by most recently updated. Supports filtering by status and category via query parameters. Limited to 200 results per request.

Data source: GET /dashboard/api/admin/support/tickets?status=open&category=technical

5.8.2 Ticket Detail (Admin View)

Shows the full ticket including customer name, email, IP address, original message, and the complete reply thread — including internal admin notes that are never visible to customers. Useful for context when multiple admins collaborate on a ticket.

Data source: GET /dashboard/api/admin/support/tickets/{id}

5.8.3 Admin Reply

Post a reply to any ticket. Replies can be marked as is_internal: true for admin-only notes that the customer never sees. Customer-visible replies are auto-translated to the customer's preferred_lang and trigger an email notification with a one-click "Mark as Resolved" link.

Status flow: First admin reply to a new ticket automatically advances status to open.

Data source: POST /dashboard/api/admin/support/tickets/{id}/reply

5.8.4 Status Management

Change a ticket's status to any valid value. Audit-logged with the admin's email.

Data source: POST /dashboard/api/admin/support/tickets/{id}/status

5.8.5 Multi-Lingual Support Flow

The support system is fully multi-lingual:

  • Customer submits ticket in their language → auto-translated to English (message_en column) for admin readability
  • Admin replies in English → auto-translated to customer's preferred_lang for email notification and dashboard display
  • Customer replies in their language → auto-translated to English for admin view
  • Translation powered by AWS Translate (real-time, not batch)

5.8.6 AutoOps Integration

When a ticket is submitted, the tbi-rhema-support Lambda is invoked asynchronously. It auto-categorizes the ticket, drafts a response, and notifies the admin with category, priority, draft, and internal notes. The analysis is stored in Valkey at support:ticket:{id} and included in the admin ticket detail response.

5.9 Translation Service Panel

Available to accounts with a Translation API key (service_type = 'translation'). Provides a complete self-service interface for submitting translation jobs, choosing AI agents, monitoring progress, and reviewing history.

5.9.1 Submission Form

The translation submission form allows customers to submit documents for translation directly from their dashboard — no API calls required.

FieldTypeDescription
Document URLURL inputPublic URL of the HTML document to translate. Must be accessible via HTTPS. Max 500 KB.
AI AgentDropdownChoose the translation agent: Best Value (Qwen3 235B — default), Efficient (Mistral Large 3), Capable (DeepSeek V3), Fast (Claude Haiku 3.5), Premium (Claude Sonnet 4.6), or Maximum (Claude Opus 4). Six agents available through Amazon Bedrock.
Target LanguagesText inputComma-separated ISO 639-1 codes (e.g., es, fr, de, ja). Supports 300+ languages.

On submission, the form calls POST /translate/quote to get an instant price quote. The customer reviews the quote (document analysis, estimated chunks, difficulty, total price) and clicks Accept & Pay to start the job.

5.9.2 Agent Selection

The agent dropdown includes a dynamic description panel that updates when the selection changes:

TierAgentSpeedBest For~Cost/Pair
Best ValueQwen3 235B DEFAULTFastBest price-to-quality. CJK, South Asian scripts, high-volume batches.~$0.05
EfficientMistral Large 3FastEuropean language specialist. Native-level French, German, Spanish, Italian.~$0.06
CapableDeepSeek V3FastTechnical docs, API references, complex reasoning at budget prices.~$0.07
FastClaude Haiku 3.5FastLatin-script languages, code/structure preservation.~$0.08
PremiumClaude Sonnet 4.6ModerateComplex scripts (Arabic, Hindi, Japanese), demanding technical docs.~$0.31
MaximumClaude Opus 4ThoroughCritical documents, legal/medical, maximum fidelity.~$1.56

All six agents are available through Amazon Bedrock and share the same sentinel protection pipeline — code blocks, brand terms, version numbers, and technical identifiers are extracted before the agent sees the document. The difference is the depth of linguistic understanding, not the safety of the content.

5.9.3 Job Status & Progress

After submitting a job, the panel shows real-time status. Customers can check the status of their currently running job at any time by returning to the Translation panel.

StateDescription
queuedJob accepted, waiting for processing capacity
runningTranslation in progress — per-language progress visible
completedAll language pairs finished successfully
partialSome pairs succeeded, some failed — retry available
failedJob failed entirely — error details available
cancelledJob was cancelled by the customer or admin

5.9.4 Translation History

The history table shows all past translation jobs for the customer's API key, with date filtering and export options (JSON, CSV, TSV, Text).

ColumnDescription
DocumentFull document filename(s). Multi-doc jobs show all filenames in a scrollable cell.
LangsNumber of target languages
AgentAI model used (e.g., qwen3-235b)
PriceTotal cost displayed with 2-decimal precision (e.g., $33.00)
StatusColor-coded: succeeded, running, queued, failed
DateSubmission date in compact format (M/D h:mm AM)
ActionsDetails link (per-pair progress view) + Download link (presigned S3 URLs, 7-day expiry)

History is queried directly from Aurora via the translation_quotes + translation_jobs tables, filtered by the customer's api_key_id. This ensures jobs appear immediately after completion — no nightly sync lag. The dashboard proxies this through GET /dashboard/api/translate/history.

Per-Pair Progress (Job Detail View)

Clicking "Details" on a history entry opens the real-time job detail view with:

  • Overall progress bar with pair count and percentage
  • Per-pair chips grouped by document — each document gets a collapsible header showing the filename and completion count (e.g., 8/11)
  • Click a document header to expand or collapse its language chips (all expanded by default)
  • Under each document header, compact language chips show the ISO code with a status icon
  • Chips are color-coded by status: green (succeeded), blue (running), red (failed/rejected), grey (skipped)
  • Tooltip on each chip shows the full LANG — filename for accessibility
  • For single-document jobs, only one group appears (collapsible but always open)
  • For multi-document jobs, the grouping makes it easy to see which documents are complete vs. still in progress
  • Download button appears when the job completes — generates presigned S3 URLs (7-day expiry) for each translated file

5.9.5 API Endpoints (Translation Service)

Customers can also interact with the Translation Service directly via API. All endpoints require a Translation API key (service_type = 'translation').

Get a Quote

POST /translate/quote
Content-Type: application/json
X-API-Key: your-translation-api-key

{
  "doc_url": "https://example.com/docs/my-document.html",
  "langs": ["es", "fr", "de", "ja", "zh"],
  "model": "qwen3-235b"
}

Response includes document analysis (size, chunks, difficulty, code blocks, diagrams), pricing breakdown (cost per chunk, per pair, markup, total), and a quote ID valid for 24 hours.

Accept a Quote (Pay & Start)

POST /translate/accept/{quote_id}
X-API-Key: your-translation-api-key

Charges the customer's payment method on file and immediately submits the translation job. Returns the job ID for status tracking.

Check Job Status

GET /translate/quote/{quote_id}
X-API-Key: your-translation-api-key

Returns the quote details including the associated job status if the quote has been accepted.

List All Quotes

GET /translate/quotes
X-API-Key: your-translation-api-key

Returns all quotes for the authenticated API key — pending, accepted, expired, and completed.

List Available Models

GET /translate/models

Public endpoint (no auth required). Returns the curated list of available AI agents with pricing, speed, quality ratings, and descriptions.

5.9.6 API Key Separation

Translation API keys and Prices API keys are distinct. Each key has a service_type field:

Key Typeservice_typeAccess
Prices API Keyprices/price, /reports, LRS endpoints
Translation API Keytranslation/translate/* endpoints

Using the wrong key type returns a clear 403 error explaining which key type is needed — not a generic "invalid key" message. This prevents confusion between the two services.

5.9.7 Brand Terms Protection

Customers can configure up to 150 brand terms that are automatically protected during every translation job. These terms are never translated or transliterated — they appear exactly as written in every target language.

The Brand Terms section appears in the Translation Service panel under Account Settings. Customers can add, edit, and remove terms at any time. Changes take effect on the next translation job — no need to re-submit existing quotes.

How It Works

  • Customer adds brand terms via the dashboard (e.g., "Acme Corp", "DataForge", "CloudSync API")
  • Terms are stored on the customer's API key in Aurora (api_keys.protected_terms JSONB column)
  • When a translation job is submitted, the customer's terms are automatically fetched and passed to the translation worker
  • The sentinel preprocessing system wraps each term with translate="no" annotations before sending text to the AI agent
  • Terms survive translation untouched — no transliteration, no localization, no modification

Limits

ConstraintValue
Maximum terms per account150
Maximum characters per term100
DuplicatesAutomatically removed on save
ScopeAccount-level — applies to all jobs, no per-request overrides

API Endpoints

MethodPathDescription
GET/dashboard/api/protected-termsReturns current terms list with count and limit
PUT/dashboard/api/protected-termsReplaces entire terms list. Body: {"terms": ["Term1", "Term2", ...]}

5.9.8 Notifications, Spend & Refund History

Three additional features enhance the Translation Service panel for customers and admins:

Notification Badge

A 🔔 icon in the dashboard header shows a red dot with the count of unread translation job completions. Clicking the badge displays a summary of recently completed jobs. Notifications are polled every 60 seconds via App.pollNotifications().

  • Badge appears in the global header — visible on all dashboard pages
  • Red dot disappears after clicking (marks notifications as seen)
  • Seen state stored in Valkey at dashboard:notifications:seen:{api_key_id} (30-day TTL)
  • Only translation job completions generate notifications — not quotes or cancellations

Translation Spend Widget

A dedicated spend section shows translation costs aggregated by month and model for the last 6 months. Customers see only their own spend; admins see all accounts. The widget auto-refreshes when a translation job completes — no manual page reload needed.

  • Stat tiles: Total Spent (or Total Revenue for admin), Jobs Completed, Language Pairs
  • Monthly breakdown table with per-model rows showing jobs, pairs, and amount (2-decimal precision)
  • Data sourced from Aurora translation_jobs + translation_quotes tables (the ledger)
  • Automatically refreshes alongside the history table when a job transitions to succeeded/partial/failed

Refund History (Admin Only)

When a refund is processed (translation or subscription), the customer receives a localized confirmation email in their preferred_lang. The email includes refund amount, original charge, refund ID, and for subscription refunds, confirms the API key has been revoked. Supported languages: English, Spanish, Portuguese, French, German, Russian, Hindi, Urdu, Arabic, Japanese, Chinese, and Italian.

Admin accounts see a refund history card in the Translation Service panel showing all refunded or partially refunded translation purchases.

ColumnDescription
DateWhen the refund was processed
DocumentOriginal document name from the quote
CustomerCustomer email (admin view only)
OriginalOriginal charge amount (USD)
RefundedRefund amount (USD)
Staterefunded or partially_refunded

API Endpoints

MethodPathDescription
GET/dashboard/api/translate/spendReturns 6-month spend breakdown by month and model. Admin sees all; customers see own.
GET/dashboard/api/translate/refundsReturns refund history. Admin-only.
GET/dashboard/api/translate/notificationsReturns unread notification count and recent completed job summaries.
POST/dashboard/api/translate/notifications/seenMarks all notifications as seen. Resets badge count to 0.

6. API Endpoints

All dashboard endpoints are served under https://api.cpmp-site.org/dashboard. Authenticated endpoints require Authorization: Bearer <token>.

6.1 Public Endpoints (No Auth)

MethodPathDescription
GET/dashboardServes the SPA HTML shell. No auth required — the SPA handles auth state client-side.
GET/dashboard/authenticateValidates magic link token, creates session, returns HTML page that writes cpmp_user JSON to localStorage and redirects to /dashboard.
POST/dashboard/api/request-linkSends magic link email. Body: {"email":"..."}. Always returns 200.

6.2 Authenticated Endpoints (Bearer Token Required)

MethodPathRole RequiredDescription
GET/dashboard/api/sessionAnyValidates token, returns email, roles, created_at, last_seen. Called on SPA load.
POST/dashboard/api/logoutAnyDeletes session from Valkey. SPA clears cpmp_user from localStorage on 200.
GET/dashboard/api/accountAnyReturns full resolved account — all relationships, roles, display name. The primary data source for all panels.
POST/dashboard/api/billing/portalAnyCreates Stripe billing portal session. Returns {"url":"..."}. Audit-logged.
GET/dashboard/api/subscription-detailsapi-subscriberLive Stripe subscription data: plan name, amount, interval, status, cancel state, current period, next payment estimate, payment method (card brand + last4), plus tier change history from Aurora.
POST/dashboard/api/subscription/reactivateapi-subscriberReverses a scheduled cancellation. Sets cancel_at_period_end=false on Stripe, clears cancel_at in Aurora. Audit-logged.
GET/dashboard/api/available-plansapi-subscriberReturns plans from tier_catalog for the customer's product family (REST or Webhook). Marks current tier. Indicates if checkout flow is needed (free tier upgrade).
POST/dashboard/api/change-planapi-subscriberSwitches subscription tier via Stripe proration. Body: {"target_tier":"..."}. Free-tier returns checkout_url. Paid-tier updates in place. Audit-logged.
GET/dashboard/api/lifetime-quoteapi-subscriberReturns lifetime upgrade eligibility and credit calculation (remaining subscription value deducted from $3,000).
GET/dashboard/api/api-key/revealapi-subscriber, translation-customer, webhook-associate, partner, or adminReturns full unmasked API key. Accepts optional ?key_id= to reveal a specific key (for multi-product accounts). Without key_id, returns the first key. Audit-logged on every call.
GET/dashboard/api/usageapi-subscriberReturns current month usage (from active key's counter), quota, and 30-day daily breakdown array (consolidated across all user's API keys via user_id).
GET/dashboard/api/giving/historydonorReturns last 12 months of Stripe charges — date, amount_usd, status, receipt_url.
GET/dashboard/api/reports/usageapi-subscriberProxies to LRS /reports/usage. Resolves user's user_id from session and passes it to LRS for consolidated cross-key history. Accepts same query params as direct LRS (asset, start_date, end_date, page, page_size, format, cached).
GET/dashboard/api/reports/summaryapi-subscriberProxies to LRS /reports/summary. Resolves user's user_id from session for consolidated history. Accepts same query params as direct LRS (start_date, end_date, format).
POST/dashboard/api/webhook/activatewebhook-associateActivates a pending_verification webhook subscription. Body: {"delivery_method":"...", "udp_host":"...", "udp_port":..., "https_url":"..."}. Requires the asset list already be configured. Skips the public API's token-echo verification since a dashboard session already proves control.
GET/dashboard/api/webhook/logwebhook-associateReturns up to 50 recent push events, grouped by (sequence_number, delivery_method), each with a nested per-asset price array.
GET/dashboard/api/webhook/reports/dailywebhook-associateDaily delivery-rollup rows for the caller's subscription, date-range filterable, 4-format export (JSON/CSV/TSV/Text).
GET/dashboard/api/webhook/reports/summarywebhook-associateAggregate delivery summary (delivery rate, asset coverage, latency) over the selected date range.
POST/dashboard/api/webhook/reports/refreshwebhook-associateForces an immediate rollup refresh. 3-day scan window, 60-second per-subscription cooldown (Redis SetNX).
GET/asset-namesNone (public)Returns {"names": {"BTC":"Bitcoin", ...}} from exchange_asset_map.display_name, used for asset-picker hover tooltips.
POST/dashboard/api/profileAnyUpdates display name. Body: {"display_name":"..."}.
POST/dashboard/api/email/changeAnyInitiates email change. Sends verification to new address. Body: {"new_email":"..."}.
GET/dashboard/api/support/ticketsAnyReturns all support tickets for the authenticated user's email. Ordered by most recent.
GET/dashboard/api/support/tickets/{ticket_number}AnyReturns full ticket detail with reply thread (excludes internal admin notes). Scoped to user's email.
POST/dashboard/api/support/tickets/{ticket_number}/replyAnyPost a customer reply. Body: {"message":"..."}. Max 10,000 chars. Auto-translates to English for admin. Notifies admin via email.
POST/dashboard/api/support/tickets/{ticket_number}/resolveAnyMark own ticket as resolved. Notifies admin. Closed tickets cannot be resolved (open a new one).
GET/dashboard/api/translate/historytranslation-subscriberReturns translation job history for the authenticated user's API key. Includes job ID, state, docs, langs, model, cost, pair counts.
GET/dashboard/api/translate/status/{job_id}translation-subscriberReturns real-time status of a specific translation job including per-language progress.
GET/dashboard/api/protected-termstranslation-subscriberReturns the customer's brand terms list (terms, count, limit of 150).
PUT/dashboard/api/protected-termstranslation-subscriberReplaces brand terms list. Body: {"terms": [...]}. Max 150 terms, 100 chars each.
GET/dashboard/api/translate/spendtranslation-subscriberReturns 6-month spend breakdown by month and model. Admin sees all accounts; customers see own spend only.
GET/dashboard/api/translate/refundsadminReturns refund history for all translation purchases. Admin-only.
GET/dashboard/api/translate/notificationstranslation-subscriberReturns unread notification count and recent completed job summaries for the authenticated user.
POST/dashboard/api/translate/notifications/seentranslation-subscriberMarks all notifications as seen. Resets badge count to 0.
GET/dashboard/api/translate/download/{job_id}translation-subscriberReturns presigned S3 download URLs for each translated file. Links expire in 7 days.

6.3 Admin-Only Endpoints

MethodPathDescription
POST/dashboard/api/admin/impersonateStart impersonation session for a target email. Body: {"email":"..."}. Returns new session token with target's roles + impersonated_by field.
POST/dashboard/api/impersonate/endEnd impersonation, restore admin session.
GET/dashboard/api/admin/analyticsAdmin-only analytics panel. Queries usage_logs directly from Aurora (no 93-day TTL). Filters: date range, asset, api_key_id, source, node. Returns detail + summary views with breakdowns by asset, source, node, api_key, and day.
GET/dashboard/api/admin/support/ticketsList all support tickets. Filters: ?status=, ?category=. Returns up to 200 tickets ordered by most recently updated.
GET/dashboard/api/admin/support/tickets/{id}Full ticket detail by UUID — includes all replies (internal notes visible), customer IP, email, name.
POST/dashboard/api/admin/support/tickets/{id}/replyPost admin reply. Body: {"message":"...", "is_internal": false}. Internal notes hidden from customer. Customer-visible replies auto-translated and emailed.
POST/dashboard/api/admin/support/tickets/{id}/statusUpdate ticket status. Body: {"status":"open"}. Valid: new, open, in_progress, awaiting_customer, resolved, closed. Audit-logged.
POST/admin/translate/refund/{quote_id}Issue full or partial refund for a translation purchase. Body (optional): {"amount": 5.00, "reason": "..."}. Processes via Stripe, sends confirmation email to customer.

6.4 Response Format — Unified Messaging Envelope (UME)

All /dashboard/api/* JSON endpoints return the standard 12-field Unified Messaging Envelope — the same structure used by the LPO and LRS APIs. There are no exceptions. Every success, every error, every auth failure uses the same shape.

6.4.1 Success Response

On success, data contains the endpoint-specific payload and error is an empty string:

{
  "status": "✅ [LPO] [us-east-2] [BeastMain] [/dashboard/api/account] [200]",
  "status_code": 200,
  "endpoint": "/dashboard/api/account",
  "cluster_node": "BeastMain",
  "region": "us-east-2",
  "language": "en",
  "api_key_id": "ak_demo123",
  "ip_address": "203.0.113.42",
  "agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbi-dashboard/v1",
  "timestamp": "2026-05-17T18:30:00Z",
  "data": {
    "email": "user@example.com",
    "display_name": "Cory Dean",
    "preferred_lang": "en",
    "created_at": "2026-01-15T00:00:00Z",
    "roles": ["api-subscriber", "donor"],
    "is_admin": false,
    "api_key": {
      "api_key_id": "uuid",
      "api_key_masked": "tbcc-****-****-****",
      "tier": "pro",
      "subscription_status": "active",
      "usage_this_month": 12847,
      "quota": 50000,
      "rate_limit_qps": 10,
      "burst_limit": 50,
      "lrs_enabled": false,
      "next_renewal": "2026-06-15"
    },
    "donor": {
      "stripe_customer_id": "cus_...",
      "amount_cents": 2500,
      "currency": "usd",
      "interval": "month",
      "status": "active",
      "next_renewal": "2026-06-01",
      "lifetime_total_usd": "125.00"
    },
    "webhook": null,
    "partner": null
  },
  "error": ""
}

6.4.2 Error Response

On error, data is null and error contains the bracket-prefixed message:

{
  "status": "🛑 [LPO] [us-east-2] [BeastMain] [/dashboard/api/account] [401]",
  "status_code": 401,
  "endpoint": "/dashboard/api/account",
  "cluster_node": "BeastMain",
  "region": "us-east-2",
  "language": "en",
  "api_key_id": "ak_demo123",
  "ip_address": "203.0.113.42",
  "agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbi-dashboard/v1",
  "timestamp": "2026-05-17T18:30:00Z",
  "data": null,
  "error": "🛑 [LPO] [us-east-2] [BeastMain] [/dashboard/api/account] [401] Session expired or invalid"
}

6.4.3 SPA Envelope Handling

The SPA's apiFetch() function handles envelope unwrapping transparently:

This means all existing panel code that consumes apiFetch results continues to work unchanged — the envelope is invisible to panel rendering logic.

6.4.4 Non-JSON Responses (Exceptions)

Two response types bypass the UME envelope by design:

7. Admin Impersonation

Admin accounts (identified by the admin_email application parameter) can impersonate any account for support and debugging purposes. Impersonation creates a new session with the target account's roles plus an impersonated_by field recording the admin's email.

PropertyBehavior
Visual indicatorRed banner always visible: "You are viewing [email]'s account as admin"
Audit loggingBoth the admin and the target account are audit-logged on impersonation start and end
Session isolationImpersonation creates a new session token — the admin's original session is preserved
End impersonationPOST /dashboard/api/impersonate/end — restores the admin's original session
AccessAdmin sees the target's full dashboard exactly as the target would see it

7.1 How to Impersonate an Account

The impersonation panel appears at the bottom of the Overview panel when logged in as admin. It is only visible when is_admin: true is returned by the account endpoint.

  1. Log in to the dashboard at https://api.cpmp-site.org/dashboard using your admin email (corydeankalani@cpmp-site.org).
  2. On the Overview panel, scroll to the bottom — you will see the Admin — Impersonate Account card.
  3. Enter the target account's email address (e.g. contact@cpmp-site.org for Homer Simpson).
  4. Click Impersonate. The SPA swaps your session token and reloads with the target account's data.
  5. A red banner appears at the top of every panel: "You are viewing [email]'s account as admin."
  6. To switch to a different account, click End Impersonation in the red banner first, then impersonate again with a new email.
  7. To return to your own dashboard, click End Impersonation. You will be prompted to sign in again — request a new magic link for your admin email.

7.2 How to Test Each Account Role

Use impersonation to see exactly what each test account sees without logging out and back in. The four test accounts and their roles:

EmailNameRoleWhat to look for
corydeankalani@cpmp-site.orgCory Dean KalaniAdmin + LifetimeImpersonation card visible, LRS Reports in sidebar, unlimited usage, no rate limits panel
contact@cpmp-site.orgHomer SimpsonPro API SubscriberUsage panel with 30-day chart (3,330 logs), amber quota bar at 62%, Rate Limits panel (10 QPS / 50 burst)
support@cpmp-site.orgBugs BunnyWebhook AssociateWebhook Configuration panel with 9 assets, UDP + HTTPS endpoints, 4,320 pushes this month
admin@cpmp-site.orgTony StarkPartnerConnection panel with PrivateLink status (Active), Asset Watchlist wizard with chip-picker, Reports link in sidebar, no rate limits

7.3 Magic Link Flow (Alternative to Impersonation)

If you want to test an account as that user would experience it (without the admin red banner), use the magic link flow directly:

  1. Open an incognito/private browser window.
  2. Go to https://api.cpmp-site.org/dashboard.
  3. Enter the test account email and click Send Login Link.
  4. Check the inbox for that email address — a magic link will arrive from The Trinity Beast <No-Reply@CPMP-Site.org>.
  5. Click the link — the dashboard opens with that account's full data, no admin banner.

Note: Magic links expire in 15 minutes and are single-use. Rate limit is 30 requests per hour per email address.

8. Data Sources

DataSourceTable / KeyNotes
API key details, usage, tierAuroraapi_keys JOIN usersIdentity anchored on users.email
30-day daily usage breakdownAurorausage_logsGrouped by day in EST timezone
Webhook configurationAurorawebhook_subscriptionsJoined via api_key_id
Donor subscription statusStripeCustomer search + subscriptions APINon-fatal if Stripe is unavailable
Donation historyStripeCharges APILast 12 months, succeeded + captured only
Billing portal URLStripeBilling Portal Sessions APICreated on demand, not cached
Admin emailAuroraapplication_parameters WHERE key = 'admin_email'Single row lookup
Sessions, magic links, rate limitsValkeysession:*, magic:*, ratelimit:*See Section 4
Audit logValkeyaudit:dashboard:{email}Sorted set, last 500 events, 90-day TTL
Dashboard session (client)BrowserlocalStorage cpmp_userJSON: token, email, name, roles, lang — cleared on logout
Language preference (client)BrowserlocalStorage cpmp_siteJSON: lang — persists across sessions, survives logout

9. Security Design

ConcernMitigation
Password exposureNo passwords — magic link only
Token theftTokens are 256-bit random, stored hashed in Valkey, transmitted only in Authorization header (not cookies)
CSRFNot applicable — Bearer tokens in Authorization header are not auto-sent by browsers
Email enumerationRequest-link always returns 200 regardless of account existence
Magic link replayAtomic GETDEL — token consumed on first use, cannot be replayed
Brute forceRate limiting: 30 magic link requests/hour per email, 60/hour per IP
Session fixationNew session token generated on every login — magic link token and session token are separate
API key exposureKey masked by default, full key only returned on explicit Reveal action, audit-logged
Impersonation abuseAdmin-only, both parties audit-logged, red banner always visible, original session preserved
XSSAll user-supplied values escaped via esc() helper before DOM insertion

10. Valkey Keys

Key PatternTypeTTLContents
session:{sha256(token)}STRING24h slidingJSON Session object — email, roles, timestamps, IP, user_agent, impersonated_by
magic:{sha256(token)}STRING15 minJSON MagicLinkPayload — email, requested_at, IP, user_agent
ratelimit:magic:email:{email}STRING1 hourInteger counter — magic link requests from this email
ratelimit:magic:ip:{ip}STRING1 hourInteger counter — magic link requests from this IP
audit:dashboard:{email}ZSET90 daysSorted set of audit events, scored by Unix ms timestamp. Max 500 entries. Events: login, logout, magic-link-requested, api-key-revealed, billing-portal-opened, impersonation-start, impersonation-end.
support:resolve:{token}STRING30 daysMaps a single-use resolve token to a ticket UUID. Consumed atomically via GETDEL when the customer clicks the email resolve link.

11. Implementation Status

ComponentStatusNotes
Magic link auth flow✅ LiveSES email, 15min TTL, single-use atomic GetDel
Session management✅ Live24h sliding TTL, Bearer token, Valkey-backed
Account resolver✅ LiveAurora + Stripe → typed relationships + roles
SPA shell + routing✅ LiveAll panels rendered client-side, role-aware sidebar
Overview panel✅ LiveAdapts to all account types
Giving Overview panel✅ LiveDonor data, Stripe portal button, invitation card when no history
Donation History panel✅ Live12 months of Stripe charges
Impact panel✅ LivePhoto gallery, static content
Profile panel✅ LiveEditable display name, email change with verification, language preference
Billing panel✅ LiveStripe portal session on demand
API Key panel✅ LiveMasked display, reveal + copy
Usage panel✅ LiveMonthly total + 30-day bar chart
Rate Limits panel✅ LiveQPS, burst, quota from the resolved API key
Webhook Configuration panel✅ LiveFull config display + dashboard-native activation flow. Asset wizard with categorized chip-picker, volume sort toggle, tier-aware limits.
Partner Connection panel✅ LivePrivateLink status card, asset watchlist wizard (uncapped), Reports sidebar link. Connection status reports "active" for provisioned keys. Live CloudWatch/TCP health probe planned for post-onboarding.
Support panel✅ LiveFull inline ticket history, submit new tickets, reply to existing tickets. Customer and Admin views with filtering.
Admin impersonation✅ LiveFull impersonation with audit trail
Admin Analytics panel✅ LiveAurora-based usage analytics (no 93-day TTL). Filters: date range, asset, api_key_id, source, node. Detail + Summary views with bar charts. Export in JSON/CSV/TSV/Text. Admin-only (sidebar "📈 Analytics" under Admin section).
LRS Reports panel✅ LiveInteractive panel with Usage/Summary tabs, date range picker, asset filter, pagination (30/60/90), export (JSON/CSV/TSV/Text). Proxies to local LRS via /dashboard/api/reports/*. Available to all tiers — same monthly report limits apply.
Webhook Delivery Log panel✅ LiveGrouped push-event table (sortable, expandable to per-asset detail) + 3-minute Activity Rollup card. Capped at the 50 most recent pushes.
Webhook Delivery Reports panel✅ LiveAurora-backed daily/summary rollup, date-range filtering, 4-format export, manual refresh with cooldown. Not LRS-gated — ownership of the subscription is the only requirement.
Webhook "Activate Delivery" flow✅ LiveDashboard-native activation of a pending_verification subscription — no separate token-verification round trip needed for an already-authenticated session.
Webhook Wizard✅ LiveCategorized asset picker with volume-sort toggle, name tooltips, and uncapped (?full=1) full-catalog mode; endpoint configuration; plan-aware limits (9 / 30 / 60 / 60 assets by tier — Professional and Enterprise share a ceiling)
Dashboard icon system✅ Live58-icon SVG sprite (Lucide-derived) replacing emoji throughout the navigation and panels. Language picker's flag emoji intentionally out of scope.
Backend error-message localization✅ LiveAll dashboard/api error responses render in the session's language via a Valkey-backed loader (dashboard:errmsg), synced nightly from S3 by BeastReconciler with an on-demand admin reload endpoint for same-day changes.
Partner connection live health⏳ Post-onboardingCloudWatch or TCP probe — relevant once a real partner creates a VPC endpoint. Current "active" status is truthful for all provisioned keys.
Dashboard i18n✅ LiveAll panel labels, button text, and status strings delivered via dashboard.* i18n namespace in all 12 language JSON files. SPA reads cpmp_site.lang from localStorage and applies translations dynamically. Fully multi-lingual.
Mobile polish⏳ PlannedResponsive layout improvements
Announcement email⏳ Pre-launchEmail all active subscribers when testing is complete

12. Role Walkthroughs — What Each Account Sees

The following panels show exactly what each test account sees when logged in to the dashboard. Data reflects the current seed state in Aurora. Each account uses a cartoon character as the display name — a convention that keeps test data obviously fictional.

Note the sidebars are identical. Every account below navigates the same seven sections, whatever they have purchased — the only difference is the Admin section on 12.1. What varies between these accounts is the content of each panel: Homer opening the Webhook panel sees an invitation to the push product, while Bugs Bunny sees his live configuration. Reading these walkthroughs side by side is the clearest way to see the invitation model at work.

✅ Dashboard is fully multi-lingual.

All panel labels, button text, status strings, and UI copy are delivered in the user's preferred_lang. The dashboard.* i18n namespace is present in all 12 language JSON files and loaded dynamically by the SPA. Language detection is a four-step chain: cpmp_user.lang (the authenticated user's own preference) → cpmp_site.lang (the site-wide picker, shared with the main website) → navigator.languages filtered to the 12 supported codes → English fallback. The dashboard is safe to announce to all subscribers regardless of language.

12.1 Cory Dean Kalani — Admin + Lifetime

The admin account. The only account that sees the Admin section (Analytics, Person View) and holds the impersonation capability. LRS Reports enabled, unlimited usage. The red impersonation banner appears when viewing another account.

corydeankalani@cpmp-site.org  ·  Sign out
Welcome back, Cory Dean Kalani
Lifetime plan · Unlimited access · LRS Reports enabled
Plan
Lifetime
Active
Usage This Month
0
Unlimited
LRS Reports
Enabled
Included
Role
Admin
Full access
Quick Actions
View API Key Usage Stats LRS Reports Open Support Ticket Manage Billing
🔐 Admin — Impersonate Account
Enter any email to view their dashboard as if you were them.
homer@example.com Impersonate

When impersonating, a red banner appears at the top of every panel:

🔴 You are viewing homer@example.com's account as admin  ·  End Impersonation
homer@example.com (impersonated)

12.2 Homer Simpson — Pro API Subscriber

A rate-limited pro subscriber. 31,204 of 50,000 requests used this month (62% — amber progress bar). 30-day usage chart shows realistic ramp-up with heavier recent activity.

contact@cpmp-site.org  ·  Sign out
Usage
Pro plan · 50,000 requests/month
This Month
31,204 requestsof 50,000
62% used · 18,796 remaining
Last 30 Days
Apr 13May 13

Rate Limits panel for Homer's pro tier:

contact@cpmp-site.org
Rate Limits
Current Limits
Requests per Second10 QPS
Burst Limit50 requests
Monthly Quota50,000 requests

12.3 Bugs Bunny — Webhook Associate

A webhook standard subscriber. 9 assets configured, 15-second push interval, both UDP and HTTPS delivery endpoints active. 4,320 pushes delivered this month.

support@cpmp-site.org  ·  Sign out
Webhook Configuration
Standard plan · 9 assets · 15s interval
Current Setup
PlanWebhook Standard
Assets Configured9
Push Interval15 seconds
HTTPS Endpointhooks.bugsbunny.io/tbi/prices
UDP Endpoint198.51.100.42:2679
Pushes This Month4,320
Configured Assets
BTCETHSOLXRPDOGEADALINKDOTAVAX
Manage Configuration
Use the asset picker to add or remove assets. Changes take effect on the next push cycle.
Edit Assets Webhook Docs

12.4 Tony Stark — Partner

An AWS PrivateLink partner account. High-volume, low-latency access via internal network. No rate limits, no monthly quota. LRS Reports included free by tier policy. Asset Watchlist wizard for dashboard-level organization (does not restrict access). Connection panel shows PrivateLink status and SLA tier.

admin@cpmp-site.org  ·  Sign out
Welcome back, Tony Stark
Partner account · PrivateLink · Enterprise SLA
Connection
Connected
PrivateLink
SLA Tier
Enterprise
99.99% uptime
Hourly Volume
200
req / hour
Rate Limits
None
Unlimited
Connection Details
Status● Connected
Endpointvpce-0f52837e401b8960e
SLA TierEnterprise
Hourly Volume200 requests / hour
Last 7 Days — Usage
May 6May 13
1,400 requests · avg 1–3ms latency · all cached · source: privatelink