Customer self-service portal — passwordless auth, role-based panels, billing, API keys, usage charts, webhooks, support, and admin impersonation.
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.
graph LR
subgraph Browser["Browser (SPA)"]
direction TB
A["/dashboard — HTML Shell"] --> B["localStorage\ncpmp_user.token"]
B --> C["JSON API Calls"]
end
subgraph LPO["LPO Server (ECS)"]
direction TB
D["SPAHandler\nserves shell"] --> E["RequireSession\nmiddleware"]
E --> F["Panel API\nhandlers"]
end
subgraph Data["Data Layer"]
G["Aurora\napi_keys · users\nusage_logs"]
H["Valkey\nsessions · magic links\naudit log"]
I["Stripe\nsubscriptions\ncharges"]
end
C -->|"Authorization: Bearer"| E
F --> G
F --> H
F --> I
Every dashboard user is resolved into an Account — a unified identity that may carry one or more typed facets. A single email address can have multiple facets simultaneously (e.g., a person who both donates to CPMP and subscribes to the API).
| Facet | Role Granted | Source | Description |
|---|---|---|---|
DonorFacet | donor | Stripe | Active recurring donation subscription via CPMP donate page |
APIKeyFacet | api-subscriber | Aurora api_keys | Active API subscription (free, pro, enterprise, unlimited, lifetime) |
WebhookFacet | webhook-associate | Aurora api_keys + webhook_subscriptions | Active webhook push subscription (starter, standard, professional, enterprise) |
PartnerFacet | partner | Aurora api_keys | AWS PrivateLink partner account |
| Admin | admin | Aurora application_parameters | Email matches admin_email parameter — grants full access + impersonation |
When an account has both a DonorFacet and at least one service facet (API, Webhook, or Partner), the sidebar shows a Giving / Services tab split. Single-facet accounts see a flat sidebar with no tabs.
flowchart TD
A["resolveAccount(email)"] --> B["Aurora: JOIN api_keys + users\nWHERE email = ? AND revoked = false"]
B --> C{tier?}
C -->|"free/pro/enterprise\nunlimited/lifetime"| D["APIKeyFacet"]
C -->|"partner"| E["PartnerFacet"]
C -->|"webhook_*"| F["WebhookFacet\n+ webhook_subscriptions query"]
A --> G["Stripe: customer search by email\n→ active subscriptions"]
G --> H["DonorFacet"]
A --> I["application_parameters\nWHERE key = 'admin_email'"]
I -->|"email matches"| J["IsAdmin = true"]
D & E & F & H & J --> K["deriveRoles()\n→ roles slice"]
K --> L["Account returned\nto auth handler"]
| Account Has | Sidebar Shows |
|---|---|
| Donor only | Giving section (Overview, History, Impact) |
| API subscriber only | Account, Billing, Support, API Key, Usage, Rate Limits (if rate-limited tier), Reports (if LRS enabled) |
| Webhook only | Account, Billing, Support, Webhook Configuration, Delivery Log |
| Partner only | Account, Billing, Support, Connection, Usage |
| Donor + any service | World tabs (❤️ Giving / ⚡ Services) — each tab shows its own sidebar section |
| Admin | All panels + impersonation capability |
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.
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 (3/hr per email, 10/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) → facets + 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
| Property | Value |
|---|---|
| Token entropy | 32 bytes (256 bits) cryptographically random via crypto/rand |
| Storage | SHA-256 hash stored in Valkey — raw token never persisted server-side |
| Magic link TTL | 15 minutes |
| Single-use enforcement | Atomic GETDEL — consumed on first use, cannot be replayed |
| Session TTL | 24 hours, sliding (extended on every authenticated request) |
| Rate limiting | 3 requests/hour per email, 10 requests/hour per IP |
| Email enumeration | Always returns 200 regardless of whether account exists |
| Transport | Authorization: Bearer <token> header — never in cookies, no CSRF needed |
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.
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 Key | TTL | Contents |
|---|---|---|
session:{hash} | 24h sliding | Email, roles, created_at, last_seen, IP, user_agent, impersonated_by (if admin session) |
magic:{hash} | 15 min | Email, requested_at, IP, user_agent |
ratelimit:magic:email:{email} | 1 hour | Request counter (max 3) |
ratelimit:magic:ip:{ip} | 1 hour | Request counter (max 10) |
audit:dashboard:{email} | 90 days | Sorted set of audit events (last 500), scored by timestamp ms |
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.
| Key | Lifetime | Structure | Written By |
|---|---|---|---|
cpmp_user | Session (cleared on logout) | | Magic link authenticate endpoint (/dashboard/authenticate) |
cpmp_site | Permanent | | i18n.js v5 (language selector flag dropdown) |
cpmp_user — Dashboard session state. Written once on magic link authentication. The SPA reads cpmp_user.token for the Authorization: Bearer header on every API call. The email, name, and roles fields enable immediate UI rendering (sidebar, welcome message, role-gated panels) without waiting for the /session API response. Cleared entirely on logout.cpmp_site — Website-wide preferences. Persists across sessions and survives logout. Currently stores only lang (the user's chosen language code). The dashboard reads cpmp_site.lang as a fallback if cpmp_user.lang is not set (e.g., before login).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.
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.
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.
graph TD
A["Account Loaded"] --> B{HasMultipleWorlds?}
B -->|"Donor + Service"| C["World Tabs\n❤️ Giving / ⚡ Services"]
B -->|"Single facet"| D["Flat sidebar"]
C -->|"Giving tab"| E["giving-overview\ngiving-history\ngiving-impact"]
C -->|"Services tab"| F["account · billing · support"]
D --> F
F --> G{APIKeyFacet?}
G -->|yes| H["api-key · usage\nrate-limits (if limited tier)\nreports (if LRS enabled)"]
F --> I{WebhookFacet?}
I -->|yes| J["webhook · webhook-log"]
F --> K{PartnerFacet?}
K -->|yes| L["connection · usage"]
F --> M{IsAdmin?}
M -->|yes| N["All panels\n+ impersonation"]
The default landing panel. Shows a welcome message, key stat tiles, and quick action buttons. Content adapts to the account's facets — a donor sees giving stats, an API subscriber sees usage stats, a partner sees connection status.
If the account has both donor and service facets, a giving bar shows lifetime total with a link to the Giving tab.
Data source: GET /dashboard/api/account (no additional API call needed).
Three panels available to accounts with a DonorFacet:
| Panel | Route Key | Contents | Data Source |
|---|---|---|---|
| Giving Overview | giving-overview | Status, monthly gift amount, total given, next renewal date, Stripe portal button | /account (DonorFacet) |
| Donation History | giving-history | Table of last 12 months of Stripe charges — date, amount, receipt link | GET /dashboard/api/giving/history |
| Impact | giving-impact | Photo gallery of CPMP mission work — medical camps, freedom moments, wheelchairs, Bible distribution, provisions, training | Static (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.
| Panel | Route Key | Contents | Data Source |
|---|---|---|---|
| Profile | account | Email (with change option), display name (editable), preferred language, member since date, roles | /account |
| Billing | billing | Stripe Customer Portal button, next renewal date. Portal allows payment method updates, invoice history, subscription management. | POST /dashboard/api/billing/portal (on button click) |
The billing portal resolves the Stripe customer ID in order: Aurora api_keys.stripe_customer_id → Stripe customer search by email. The portal session URL is returned and opened in a new tab. Return URL is https://cpmp-site.org/dashboard.
Available to accounts with an APIKeyFacet.
| Panel | Route Key | Contents | Data Source |
|---|---|---|---|
| API Key | api-key | Masked 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) |
| Usage | usage | Current month request count, quota progress bar (color-coded: green/amber/red), 30-day daily bar chart | GET /dashboard/api/usage |
| Rate Limits | rate-limits | QPS limit, burst limit, monthly quota. Only shown for rate-limited tiers (free, pro, enterprise). Hidden for unlimited/lifetime. | /account (APIKeyFacet) |
| LRS Reports | reports | Interactive 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 on port 9090 using the user's API key. Available to all API key holders regardless of tier — same monthly report limits apply. | GET /dashboard/api/reports/usageGET /dashboard/api/reports/summary |
The API key is masked by default (tbcc-****-****-**** style). The Reveal action calls /api-key/reveal, which requires api-subscriber, webhook-associate, partner, or admin role. 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.
Available to accounts with a WebhookFacet.
| Panel | Route Key | Contents | Status |
|---|---|---|---|
| Webhook Configuration | webhook | Current plan, asset count, push interval, HTTPS endpoint, UDP endpoint, last delivery timestamp. Includes categorized asset picker (7 groups: Major Currencies, DeFi, Layer 2, AI & Data, Gaming & NFTs, Meme Coins, Infrastructure) and endpoint configuration wizard. | Live — full management |
| Delivery Log | webhook-log | Recent delivery history | Stub — coming in next update |
The webhook configuration panel includes a categorized asset picker that groups all 150 available assets into 7 categories for easy selection. Plan-tier limits are enforced (e.g., starter = 3 assets, standard = 10, professional = 25, enterprise = 50).
Available to accounts with a PartnerFacet.
| Field | Description |
|---|---|
| Connection Status | connected / degraded / disconnected (color-coded green/amber/red) |
| PrivateLink Endpoint | AWS PrivateLink endpoint identifier |
| SLA Tier | Partner SLA level |
| Hourly Volume | Requests per hour |
Connection status is currently static ("unknown"). Live health data via CloudWatch or TCP probe is planned for a future update.
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.
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
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}
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
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
| Status | Meaning |
|---|---|
new | Just submitted, not yet reviewed by admin |
open | Admin has replied or customer re-opened |
in_progress | Admin is actively working on it |
awaiting_customer | Admin is waiting for customer response |
resolved | Marked resolved by customer or admin |
closed | Permanently closed — no further replies allowed from dashboard |
| Category | Description |
|---|---|
general | General inquiry |
api-technical | API integration or technical issue |
billing | Billing, subscription, or payment question |
bug-report | Bug or unexpected behavior |
feature-request | Feature suggestion |
mission-donations | CPMP mission or donation inquiry |
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.
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
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}
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
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
The support system is fully multi-lingual:
message_en column) for admin readabilitypreferred_lang for email notification and dashboard displayWhen a ticket is submitted, the tbi-raima-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.
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.
The translation submission form allows customers to submit documents for translation directly from their dashboard — no API calls required.
| Field | Type | Description |
|---|---|---|
| Document URL | URL input | Public URL of the HTML document to translate. Must be accessible via HTTPS. Max 500 KB. |
| AI Agent | Dropdown | Choose the translation agent: Good (Haiku 3.5), Better (Sonnet 4.6), or Best (Opus 4) |
| Target Languages | Text input | Comma-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.
The agent dropdown includes a dynamic description panel that updates when the selection changes:
| Tier | Agent | Speed | Best For | ~Cost/Pair |
|---|---|---|---|---|
| Good | Claude Haiku 3.5 | Fast | Latin-script languages, batch jobs, cost-sensitive | $0.40 |
| Better | Claude Sonnet 4.6 | Moderate | Complex scripts (Arabic, Hindi, Japanese), technical docs. Default. | $1.65 |
| Best | Claude Opus 4 | Thorough | Critical documents, legal/medical, maximum fidelity | $8.00 |
All three agents 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.
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.
| State | Description |
|---|---|
queued | Job accepted, waiting for processing capacity |
running | Translation in progress — per-language progress visible |
completed | All language pairs finished successfully |
partial | Some pairs succeeded, some failed — retry available |
failed | Job failed entirely — error details available |
cancelled | Job was cancelled by the customer or admin |
The history tab shows all past translation jobs for the customer's API key. Each entry includes:
History is queried from Aurora via the translation_jobs table, filtered by the customer's api_key_id. The dashboard proxies this through GET /dashboard/api/translate/history.
Customers can also interact with the Translation Service directly via API. All endpoints require a Translation API key (service_type = 'translation').
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": "claude-sonnet-4.6"
}
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.
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.
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.
GET /translate/quotes
X-API-Key: your-translation-api-key
Returns all quotes for the authenticated API key — pending, accepted, expired, and completed.
GET /translate/models
Public endpoint (no auth required). Returns the curated list of available AI agents with pricing, speed, quality ratings, and descriptions.
Translation API keys and Prices API keys are distinct. Each key has a service_type field:
| Key Type | service_type | Access |
|---|---|---|
| Prices API Key | prices | /price, /reports, LRS endpoints |
| Translation API Key | translation | /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.
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.
api_keys.protected_terms JSONB column)translate="no" annotations before sending text to the AI agent| Constraint | Value |
|---|---|
| Maximum terms per account | 150 |
| Maximum characters per term | 100 |
| Duplicates | Automatically removed on save |
| Scope | Account-level — applies to all jobs, no per-request overrides |
| Method | Path | Description |
|---|---|---|
GET | /dashboard/api/protected-terms | Returns current terms list with count and limit |
PUT | /dashboard/api/protected-terms | Replaces entire terms list. Body: {"terms": ["Term1", "Term2", ...]} |
Three additional features enhance the Translation Service panel for customers and admins:
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().
dashboard:notifications:seen:{api_key_id} (30-day TTL)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.
translation_jobs table (the ledger)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.
| Column | Description |
|---|---|
| Date | When the refund was processed |
| Document | Original document name from the quote |
| Customer | Customer email (admin view only) |
| Original | Original charge amount (USD) |
| Refunded | Refund amount (USD) |
| State | refunded or partially_refunded |
| Method | Path | Description |
|---|---|---|
GET | /dashboard/api/translate/spend | Returns 6-month spend breakdown by month and model. Admin sees all; customers see own. |
GET | /dashboard/api/translate/refunds | Returns refund history. Admin-only. |
GET | /dashboard/api/translate/notifications | Returns unread notification count and recent completed job summaries. |
POST | /dashboard/api/translate/notifications/seen | Marks all notifications as seen. Resets badge count to 0. |
All dashboard endpoints are served under https://api.cpmp-site.org/dashboard. Authenticated endpoints require Authorization: Bearer <token>.
| Method | Path | Description |
|---|---|---|
GET | /dashboard | Serves the SPA HTML shell. No auth required — the SPA handles auth state client-side. |
GET | /dashboard/authenticate | Validates magic link token, creates session, returns HTML page that writes cpmp_user JSON to localStorage and redirects to /dashboard. |
POST | /dashboard/api/request-link | Sends magic link email. Body: {"email":"..."}. Always returns 200. |
| Method | Path | Role Required | Description |
|---|---|---|---|
GET | /dashboard/api/session | Any | Validates token, returns email, roles, created_at, last_seen. Called on SPA load. |
POST | /dashboard/api/logout | Any | Deletes session from Valkey. SPA clears cpmp_user from localStorage on 200. |
GET | /dashboard/api/account | Any | Returns full resolved account — all facets, roles, display name. The primary data source for all panels. |
POST | /dashboard/api/billing/portal | Any | Creates Stripe billing portal session. Returns {"url":"..."}. Audit-logged. |
GET | /dashboard/api/api-key/reveal | api-subscriber, webhook-associate, partner, or admin | Returns full unmasked API key. Audit-logged on every call. |
GET | /dashboard/api/usage | api-subscriber | Returns current month usage, quota, and 30-day daily breakdown array. |
GET | /dashboard/api/giving/history | donor | Returns last 12 months of Stripe charges — date, amount_usd, status, receipt_url. |
GET | /dashboard/api/reports/usage | api-subscriber | Proxies to LRS /reports/usage. Resolves user's API key from session. Accepts same query params as direct LRS (asset, start_date, end_date, page, page_size, format, cached). |
GET | /dashboard/api/reports/summary | api-subscriber | Proxies to LRS /reports/summary. Resolves user's API key from session. Accepts same query params as direct LRS (start_date, end_date, format). |
POST | /dashboard/api/profile | Any | Updates display name. Body: {"display_name":"..."}. |
POST | /dashboard/api/email/change | Any | Initiates email change. Sends verification to new address. Body: {"new_email":"..."}. |
GET | /dashboard/api/support/tickets | Any | Returns all support tickets for the authenticated user's email. Ordered by most recent. |
GET | /dashboard/api/support/tickets/{ticket_number} | Any | Returns full ticket detail with reply thread (excludes internal admin notes). Scoped to user's email. |
POST | /dashboard/api/support/tickets/{ticket_number}/reply | Any | Post 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}/resolve | Any | Mark own ticket as resolved. Notifies admin. Closed tickets cannot be resolved (open a new one). |
GET | /dashboard/api/translate/history | translation-subscriber | Returns 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-subscriber | Returns real-time status of a specific translation job including per-language progress. |
GET | /dashboard/api/protected-terms | translation-subscriber | Returns the customer's brand terms list (terms, count, limit of 150). |
PUT | /dashboard/api/protected-terms | translation-subscriber | Replaces brand terms list. Body: {"terms": [...]}. Max 150 terms, 100 chars each. |
GET | /dashboard/api/translate/spend | translation-subscriber | Returns 6-month spend breakdown by month and model. Admin sees all accounts; customers see own spend only. |
GET | /dashboard/api/translate/refunds | admin | Returns refund history for all translation purchases. Admin-only. |
GET | /dashboard/api/translate/notifications | translation-subscriber | Returns unread notification count and recent completed job summaries for the authenticated user. |
POST | /dashboard/api/translate/notifications/seen | translation-subscriber | Marks all notifications as seen. Resets badge count to 0. |
| Method | Path | Description |
|---|---|---|
POST | /dashboard/api/admin/impersonate | Start impersonation session for a target email. Body: {"email":"..."}. Returns new session token with target's roles + impersonated_by field. |
POST | /dashboard/api/impersonate/end | End impersonation, restore admin session. |
GET | /dashboard/api/admin/analytics | Admin-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/tickets | List 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}/reply | Post 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}/status | Update 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. |
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.
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": ""
}
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"
}
The SPA's apiFetch() function handles envelope unwrapping transparently:
json.data — panel code never sees the enveloperes.status before JSON parsing — clears session, shows loginerror field (strips the bracket prefix) and throws it as an Error for the panel to display in a flash messageThis means all existing panel code that consumes apiFetch results continues to work unchanged — the envelope is invisible to panel rendering logic.
Two response types bypass the UME envelope by design:
format=csv, format=tsv, format=text on admin analytics) — these return raw file content with appropriate Content-Type and Content-Disposition headers/dashboard/api/reports/*) — these pass through the LRS response verbatim, which is already UME-wrapped by the LRS serverAdmin 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.
| Property | Behavior |
|---|---|
| Visual indicator | Red banner always visible: "You are viewing [email]'s account as admin" |
| Audit logging | Both the admin and the target account are audit-logged on impersonation start and end |
| Session isolation | Impersonation creates a new session token — the admin's original session is preserved |
| End impersonation | POST /dashboard/api/impersonate/end — restores the admin's original session |
| Access | Admin sees the target's full dashboard exactly as the target would see it |
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.
https://api.cpmp-site.org/dashboard using your admin email (corydeankalani@cpmp-site.org).contact@cpmp-site.org for Homer Simpson).Use impersonation to see exactly what each test account sees without logging out and back in. The four test accounts and their roles:
| Name | Role | What to look for | |
|---|---|---|---|
corydeankalani@cpmp-site.org | Cory Dean Kalani | Admin + Lifetime | Impersonation card visible, LRS Reports in sidebar, unlimited usage, no rate limits panel |
contact@cpmp-site.org | Homer Simpson | Pro API Subscriber | Usage panel with 30-day chart (3,330 logs), amber quota bar at 62%, Rate Limits panel (10 QPS / 50 burst) |
support@cpmp-site.org | Bugs Bunny | Webhook Associate | Webhook Configuration panel with 9 assets, UDP + HTTPS endpoints, 4,320 pushes this month |
admin@cpmp-site.org | Tony Stark | Partner | Connection panel with PrivateLink status, 7-day usage chart (1,400 logs), no rate limits |
If you want to test an account as that user would experience it (without the admin red banner), use the magic link flow directly:
https://api.cpmp-site.org/dashboard.The Trinity Beast <No-Reply@CPMP-Site.org>.Note: Magic links expire in 15 minutes and are single-use. Rate limit is 3 requests per hour per email address.
| Data | Source | Table / Key | Notes |
|---|---|---|---|
| API key details, usage, tier | Aurora | api_keys JOIN users | Identity anchored on users.email |
| 30-day daily usage breakdown | Aurora | usage_logs | Grouped by day in EST timezone |
| Webhook configuration | Aurora | webhook_subscriptions | Joined via api_key_id |
| Donor subscription status | Stripe | Customer search + subscriptions API | Non-fatal if Stripe is unavailable |
| Donation history | Stripe | Charges API | Last 12 months, succeeded + captured only |
| Billing portal URL | Stripe | Billing Portal Sessions API | Created on demand, not cached |
| Admin email | Aurora | application_parameters WHERE key = 'admin_email' | Single row lookup |
| Sessions, magic links, rate limits | Valkey | session:*, magic:*, ratelimit:* | See Section 4 |
| Audit log | Valkey | audit:dashboard:{email} | Sorted set, last 500 events, 90-day TTL |
| Dashboard session (client) | Browser | localStorage cpmp_user | JSON: token, email, name, roles, lang — cleared on logout |
| Language preference (client) | Browser | localStorage cpmp_site | JSON: lang — persists across sessions, survives logout |
| Concern | Mitigation |
|---|---|
| Password exposure | No passwords — magic link only |
| Token theft | Tokens are 256-bit random, stored hashed in Valkey, transmitted only in Authorization header (not cookies) |
CSRF | Not applicable — Bearer tokens in Authorization header are not auto-sent by browsers |
| Email enumeration | Request-link always returns 200 regardless of account existence |
| Magic link replay | Atomic GETDEL — token consumed on first use, cannot be replayed |
| Brute force | Rate limiting: 3 magic link requests/hour per email, 10/hour per IP |
| Session fixation | New session token generated on every login — magic link token and session token are separate |
| API key exposure | Key masked by default, full key only returned on explicit Reveal action, audit-logged |
| Impersonation abuse | Admin-only, both parties audit-logged, red banner always visible, original session preserved |
XSS | All user-supplied values escaped via esc() helper before DOM insertion |
| Key Pattern | Type | TTL | Contents |
|---|---|---|---|
session:{sha256(token)} | STRING | 24h sliding | JSON Session object — email, roles, timestamps, IP, user_agent, impersonated_by |
magic:{sha256(token)} | STRING | 15 min | JSON MagicLinkPayload — email, requested_at, IP, user_agent |
ratelimit:magic:email:{email} | STRING | 1 hour | Integer counter — magic link requests from this email |
ratelimit:magic:ip:{ip} | STRING | 1 hour | Integer counter — magic link requests from this IP |
audit:dashboard:{email} | ZSET | 90 days | Sorted 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} | STRING | 30 days | Maps a single-use resolve token to a ticket UUID. Consumed atomically via GETDEL when the customer clicks the email resolve link. |
| Component | Status | Notes |
|---|---|---|
| Magic link auth flow | ✅ Live | SES email, 15min TTL, single-use atomic GetDel |
| Session management | ✅ Live | 24h sliding TTL, Bearer token, Valkey-backed |
| Account resolver | ✅ Live | Aurora + Stripe → typed facets + roles |
| SPA shell + routing | ✅ Live | All panels rendered client-side, role-aware sidebar |
| Overview panel | ✅ Live | Adapts to all account types |
| Giving Overview panel | ✅ Live | DonorFacet data, Stripe portal button |
| Donation History panel | ✅ Live | 12 months of Stripe charges |
| Impact panel | ✅ Live | Photo gallery, static content |
| Profile panel | ✅ Live | Editable display name, email change with verification, language preference |
| Billing panel | ✅ Live | Stripe portal session on demand |
| API Key panel | ✅ Live | Masked display, reveal + copy |
| Usage panel | ✅ Live | Monthly total + 30-day bar chart |
| Rate Limits panel | ✅ Live | QPS, burst, quota from APIKeyFacet |
| Webhook Configuration panel | ✅ Live (read-only) | Shows current config — wizard coming next |
| Partner Connection panel | ✅ Live (static) | Live health data deferred |
| Support panel | ✅ Live | Full inline ticket history, submit new tickets, reply to existing tickets. Customer and Admin views with filtering. |
| Admin impersonation | ✅ Live | Full impersonation with audit trail |
| Admin Analytics panel | ✅ Live | Aurora-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 | ✅ Live | Interactive 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 | ⏳ Stub | Coming in next dashboard session |
| Webhook Wizard | ✅ Live | Categorized asset picker (7 groups), endpoint configuration, plan-aware limits |
| Partner connection live health | ⏳ Planned | CloudWatch or TCP probe |
| Dashboard i18n | ✅ Live | All 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 | ⏳ Planned | Responsive layout improvements |
| Announcement email | ⏳ Pre-launch | Email all active subscribers when testing is complete |
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.
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 follows the same flow as the main site: cpmp_site.lang in localStorage → browser language → English fallback. The dashboard is safe to announce to all subscribers regardless of language.
The admin account. Sees all panels, has the impersonation capability, LRS Reports enabled, and unlimited usage. The red impersonation banner appears when viewing another account.
When impersonating, a red banner appears at the top of every panel:
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.
Rate Limits panel for Homer's pro tier:
A webhook standard subscriber. 9 assets configured, 15-second push interval, both UDP and HTTPS delivery endpoints active. 4,320 pushes delivered this month.
An AWS PrivateLink partner account. High-volume, low-latency access via internal network. No rate limits, no monthly quota. Usage panel shows 7 days of PrivateLink traffic across 15 assets.