From 00bdbfa06b6cfb542b24456a7794ce5126a828a5 Mon Sep 17 00:00:00 2001 From: ghwmelite-dotcom Date: Wed, 1 Jul 2026 22:49:30 +0000 Subject: [PATCH] feat(downloads): inline EA credentials on each card + fix entity regressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bring account credentials to the point of use so a journaling-only user no longer has to hunt on the copy-trading ("Copier") page for keys. - Add an "EA Credentials" panel to every EA download card showing the matching account's Account ID + API Key with Copy buttons. The one-time API Secret is exposed via a "Regenerate to reveal secret" action (reuses POST /accounts/:id/regenerate-keys), which reveals a fresh Key + Secret once with a save-now warning. Cards with no matching account link to the Copier page instead. - Add a shared "Where to find these" callout to the Master/Journal setup steps, and use the real page name ("Copier") consistently. - Fix two rendering bugs shipped in #2: `—` / `→` were placed inside JS string literals (journal card description, troubleshooting), where they render literally instead of as — / →. JS strings now use the real character; JSX text keeps HTML entities. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/web/src/pages/DownloadsPage.tsx | 155 +++++++++++++++++++++++++-- 1 file changed, 148 insertions(+), 7 deletions(-) diff --git a/apps/web/src/pages/DownloadsPage.tsx b/apps/web/src/pages/DownloadsPage.tsx index ad069b2..bbd2e61 100644 --- a/apps/web/src/pages/DownloadsPage.tsx +++ b/apps/web/src/pages/DownloadsPage.tsx @@ -1,9 +1,10 @@ import { useState, useEffect } from 'react'; -import { Upload, Download, BookOpen, ChevronDown, ChevronRight, AlertTriangle, CheckCircle, Package, FileCode2 } from 'lucide-react'; +import { Upload, Download, BookOpen, ChevronDown, ChevronRight, AlertTriangle, CheckCircle, Package, FileCode2, KeyRound, RefreshCw, Check, Copy } from 'lucide-react'; import { Card } from '@/components/ui/Card'; import { Badge } from '@/components/ui/Badge'; import { Button } from '@/components/ui/Button'; import { API_BASE } from '@/lib/constants'; +import { api } from '@/lib/api'; import { useAccountsStore, type Account } from '@/stores/accounts'; import { useAuthStore } from '@/stores/auth'; @@ -14,6 +15,24 @@ const SOURCE_ZIP_NAMES: Record<'master' | 'follower' | 'journal', string> = { journal: 'TradeJournal_Sync_Source.zip', }; +/** Shared "where to find your credentials" callout, reused across the config steps. */ +const credentialsHint = ( +
+ +
+

Where to find these

+

+ Open the{' '} + Copier{' '} + page. On each account card, your Account ID and API Key are shown with a one-tap Copy button. +

+

+ Your API Secret is revealed only once — on the credentials screen shown immediately after you create the account. If you did not save it, click Regenerate keys on the account card to issue a fresh API Key + Secret (again shown once). +

+
+
+); + /* ------------------------------------------------------------------ */ /* Setup Guide Data */ /* ------------------------------------------------------------------ */ @@ -38,7 +57,7 @@ const setupSteps: SetupStep[] = [
  • - API Key and Secret from the Accounts page + API Key, API Secret and Account ID from the Copier page (see Step 4)
  • ), @@ -139,7 +158,7 @@ const setupSteps: SetupStep[] = [ API_Key - Your API key from dashboard + From the Copier page (Copy button) er_abc123... @@ -155,11 +174,12 @@ const setupSteps: SetupStep[] = [ AccountID Your master account ID - (from dashboard) + (Copier page) + {credentialsHint} ), }, @@ -262,6 +282,8 @@ const setupSteps: SetupStep[] = [ + {credentialsHint} +

    @@ -316,7 +338,7 @@ const troubleshootingItems = [ }, { problem: 'Error 4060 in Experts tab', - solution: 'The URL is not whitelisted. Go to Tools → Options → Expert Advisors and add https://signal.edgerelay.io to the allowed URLs.', + solution: 'The URL is not whitelisted. Go to Tools → Options → Expert Advisors and add https://signal.edgerelay.io to the allowed URLs.', }, { problem: "'Trade context busy' error", @@ -370,6 +392,123 @@ function AccordionItem({ /* EA Download Card */ /* ------------------------------------------------------------------ */ +function CredCopyButton({ text, label }: { text: string; label: string }) { + const [copied, setCopied] = useState(false); + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } catch { + /* clipboard unavailable — ignore */ + } + }; + return ( + + ); +} + +function CredRow({ label, value, valueClass }: { label: string; value: string; valueClass: string }) { + return ( +

    + {label} +
    + {value} + +
    +
    + ); +} + +/** + * Inline EA credentials for a card's matching account: Account ID + API Key + * are always available from the account list; the API Secret is one-time, so + * we expose a Regenerate action that reveals a fresh Key + Secret once. + */ +function CredentialsPanel({ account }: { account: Account | undefined }) { + const fetchAccounts = useAccountsStore((s) => s.fetchAccounts); + const [revealed, setRevealed] = useState<{ api_key: string; api_secret: string } | null>(null); + const [regenerating, setRegenerating] = useState(false); + const [regenError, setRegenError] = useState(null); + + if (!account) { + return ( +
    +
    + + EA Credentials +
    +

    + No account yet.{' '} + Create one on the Copier page{' '} + to generate your Account ID, API Key and Secret. +

    +
    + ); + } + + const handleRegenerate = async () => { + if (!window.confirm(`Regenerate API keys for "${account.alias}"?\n\nThe current API Key and Secret stop working immediately - any EA already using them must be updated with the new values.`)) return; + setRegenError(null); + setRegenerating(true); + const res = await api.post<{ id: string; api_key: string; api_secret: string }>(`/accounts/${account.id}/regenerate-keys`); + if (res.data) { + setRevealed({ api_key: res.data.api_key, api_secret: res.data.api_secret }); + await fetchAccounts(); + } else { + setRegenError(res.error?.message ?? 'Could not regenerate keys. Please try again.'); + } + setRegenerating(false); + }; + + return ( +
    +
    + + EA Credentials + {account.alias} +
    + + + + + {revealed ? ( + <> + +

    Copy the secret now — it is shown only this once.

    + + ) : ( +
    + API Secret + shown once at creation +
    + )} + +
    + +
    + + {regenError &&

    {regenError}

    } +
    + ); +} + function EADownloadCard({ type, accounts, @@ -398,7 +537,7 @@ function EADownloadCard({ const handleDownload = async () => { if (!matchingAccount) { - setError(`Create a ${type} account first on the Accounts page.`); + setError(`Create a ${type} account first on the Copier page.`); return; } @@ -480,7 +619,7 @@ function EADownloadCard({

    {isJournal - ? 'Install on any MT5 account. Syncs every trade to your journal with zero drops — real-time capture + history catch-up.' + ? 'Install on any MT5 account. Syncs every trade to your journal with zero drops — real-time capture + history catch-up.' : isMaster ? 'Install on your master MT5 account. Captures and sends trade signals to the edge network.' : 'Install on each follower account. Receives signals and executes trades automatically.'} @@ -517,6 +656,8 @@ function EADownloadCard({

    + + {error && (