Files
trumpsignal-frontend/AGENTS.md
T

19 KiB
Raw Blame History

Trump Alpha — Frontend (trumpsignal.com)

Next.js 16 dashboard for the Trump Alpha backend. Real-time signals, Hyperliquid position management UI, KOL talks-vs-trades view, Macro Vibes regime panel. Read-only for the public; wallet-bound features (Pro) for auto-trade subscribers.

This is the AI-readable entry doc. Read this first on entering the repo.


What this repo is

  • Pure frontend — Next.js 16 App Router, TypeScript, server components + client components, deployed to Vercel.
  • Talks to a Python backend at https://api.trumpsignal.com (or localhost:8000 in dev). All trading state, signals, AI scoring, Hyperliquid integration lives in the sibling ../backend repo. See its AGENTS.md.
  • No server-side trading logic lives here. Frontend is a thin layer over the API + WebSocket — even "open a position" routes to a backend endpoint that does the signed-request verification + HL call.

Stack quirks (Next.js 16 specific)

  • proxy.ts replaces middleware.ts in Next.js 16. Routes that need CORS / locale rewriting go through proxy.ts at the root.
  • params is async (Promise<{...}>) in all dynamic routes. ALL page components await it:
    export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
      const { locale } = await params
      ...
    }
    
    Forgetting the await redirects to /undefined/... silently. Several pages were broken this way pre-v2.0 — be alert if you copy older patterns.
  • i18n is shelved. The [locale] segment exists, messages/zh.json exists, but Chinese branches are kept as dead code with a comment // i18n shelved — see messages/zh.json. Don't activate without an ADR. Currently isZh = false is hardcoded in metadata generators.
  • runtime = 'edge' on a few routes (app/icon.tsx, OG image generator). Edge runtime has no fs / no Node APIs — don't import server-side libs there.

Route map

app/
├── layout.tsx              Root layout: JSON-LD schema.org, fonts, meta
├── page.tsx                / (root) → marketing / landing page
├── icon.tsx                Dynamic favicon (α on dark bg, edge runtime)
├── apple-icon.tsx          PWA install icon
├── opengraph-image.tsx     OG card generator (1200x630)
├── manifest.ts             PWA manifest
├── sitemap.ts              SEO sitemap.xml
├── robots.ts               robots.txt
└── api/
    └── proxy/[...path]/    Catch-all proxy to backend (CORS workaround)

app/[locale]/
├── page.tsx                Landing — pinned signals, live ticker, Trump feed
├── DashboardClient.tsx     Main 'use client' wrapper for the home dashboard
├── trump/page.tsx          /en/trump — Trump Truth Social signal stream
├── macro/page.tsx          /en/macro — Macro Vibes regime panel ★ renamed from /btc
├── kol/page.tsx            /en/kol — KOL talks-vs-trades
├── trades/page.tsx         /en/trades — closed trade history
├── posts/page.tsx          /en/posts — all Posts (signal + Trump)
├── archive/page.tsx        /en/archive — historical / paginated
├── settings/page.tsx       /en/settings — wallet connect, HL key, prefs
├── analytics/page.tsx      /en/analytics — performance metrics
├── case-studies/page.tsx   /en/case-studies — documented event walkthroughs
├── methodology/page.tsx    /en/methodology — how signals work
├── glossary/page.tsx       /en/glossary — AHR999, MVRV-Z, KOL, etc.
├── privacy/page.tsx
├── terms/page.tsx
├── contact/page.tsx
└── globals.css             Design system tokens + .macro-* classes

Components map (the meaningful ones)

components/
├── nav/Navbar.tsx              Top nav + tabs (Trump | Macro Vibes | KOL ...)
├── signals/
│   ├── SourceChips.tsx         Filter chips per source
│   └── SystemControl.tsx       Sys1/sys2 toggle widgets, Auto-Trade switch
├── dashboard/
│   ├── PostCards.tsx           Trump post cards
│   ├── SignalMonitor.tsx       Breakout Monitor (ETH/LINK 5m, WS-driven).
│   │                             UNMOUNTED 2026-06-12 — backend scanner is
│   │                             disabled and its 5-min poll job unscheduled.
│   │                             Kept for revival; see Open known issues.
│   └── ChartPanel.tsx          Price chart panel
├── btc/MacroPanel.tsx          ★ 8-indicator Macro Vibes layout (4 sections,
│                                 composite needle, threshold chips, peak-trail viz)
├── positions/
│   └── OpenPositions.tsx       Polls /positions/open + /positions/today every 15s
│                                 Includes grow toggle + manual close button
├── trades/
│   ├── TradeTable.tsx          Closed trade history table (dynamic asset filter)
│   └── BotConfigPanel.tsx      Bot configuration (SL, TP, leverage, paper mode).
│                                 Includes 3-step onboarding stepper + inline Auto-Trade toggle.
├── telegram/
│   └── TelegramCard.tsx        Settings — connect via 6-char code
├── wallet/
│   └── SignConfirmSheet.tsx    EIP-191 signed request preview
├── seo/Breadcrumbs.tsx         Structured breadcrumb nav for SEO
├── ui/
│   ├── InfoTip.tsx             CSS-only tooltip with `?` icon
│   ├── PageHint.tsx            Strong page subtitle
│   ├── Pagination.tsx          Reusable pagination control
│   └── TradeAlertBanner.tsx    Fixed-position WS alert banner (trade_alert events).
│                                 Auto-dismisses after 10s. Filtered by wallet address.
└── lib/wsContext.tsx           WebSocket singleton context (replaces WsProvider)

API layer

All API calls go through lib/api.ts. Single fetchJson<T> wrapper:

  • Reads base URL from env (NEXT_PUBLIC_API_URL) or proxies through /api/proxy/[...path]/ for same-origin CORS-free calls.
  • Wallet-scoped reads (/user/.../public, /positions/open) pass wallet as query param + signed ts/sig.
  • Mutations (subscribe, manual_close, set_auto_trade, manual-window) build an EIP-191 signed payload via wagmi → POST signed envelope.

Auth model: no sessions, no cookies. Wallet address + signed timestamp

  • signature on every mutating call. Verification lives in the backend (signed_request.verify_signed_request). The same wallet that signed the HL API key envelope at subscribe time is the only one that can later mutate the subscription.

State management

  • One small global storestore/dashboard.ts (Zustand, useDashboardStore). Holds cross-page UI/session state: selected post, chart asset/timeframe, wallet address, subscription + bot-readiness flags, masked HL key hint, and the live-price map. Everything else stays tree-local (state + props) — don't grow this store into a catch-all; page-specific data belongs in the page.
  • SWR-style cache in lib/cache.ts for the API-heavy pages.
  • WebSocket singleton via WsProvider — used by SignalMonitor and live price tickers.
  • localStorage holds wallet address only (for re-connect UX). HL API keys NEVER touch the client — they're submitted once at subscribe, sent encrypted, and decrypted only inside the backend.

Design system

Tokens in app/[locale]/globals.css:

  • Colorsoklch() color space throughout. Brand orange = #f5a524, brand dark = #0a0907. var(--up) green / var(--down) red / var(--ink-1..3) text tiers.
  • Dark mode via [data-theme="dark"] attribute on <html>. Most components use oklch(L% c h) so dark/light switching is automatic.
  • Macro Vibes specific — extensive .macro-* classes for the 8-indicator layout. Threshold chips have :not(.active) { opacity: 0.45 } and .active is tone-filled (green/red/amber).

Things that LOOK like bugs but aren't

  • Old code references "/en/btc" — Macro Vibes was renamed from "BTC Signal" in v2.0. All routes are now /en/macro. app/[locale]/btc/page.tsx EXISTS intentionally as a permanent redirect to /en/macro — that's fine. If you find a stale /en/btc hardcoded link anywhere else, it's a real bug — fix it.
  • isZh = false in metadata generators — i18n was shelved pre-launch. See "Stack quirks" above.
  • Multiple proxy.ts patterns — Next.js 16 split middleware behaviour. Keep proxy logic in the single proxy.ts at root.
  • 'use client' directives on many pages — necessary for client-side WS / wallet interactions. Don't try to convert without verifying the WS
    • wagmi imports work in server components.
  • telegram.send_message accepting int | str for chat_id — the backend uses string form for the public channel ("@trumpalpha") and integer for private user chats. Both are valid; no frontend change needed.
  • x-forwarded-for is deleted in the proxy — KNOWN BUG (see below). Don't "fix" it by just removing the delete; the correct fix is to relay the real client IP. See Open Known Issues.

Open known issues (frontend)

  • Rate limit bypass via proxy (FIXED 2026-05-29): route.ts no longer deletes x-forwarded-for. It now reads the real client IP from the incoming x-forwarded-for or x-real-ip header (set by Vercel/CDN) and sets it on the upstream request so slowapi can distinguish individual clients.

  • signMessageAsync in OpenPositions polling deps (MEDIUM, FIXED 2026-05-29): Removed signMessageAsync and isZh from the useEffect dependency array in components/positions/OpenPositions.tsx. Neither is used inside the effect; including them caused wagmi reference churn to restart the 15s poll timer.

  • Stale types after BUG-08/BUG-14 (FE-0106, FIXED 2026-05-29):

    • types/index.ts: price_impact.asset widened from 'BTC' | 'ETH' to string (BUG-14 now tracks the actually-traded asset which may be SOL/TRUMP/etc.).
    • lib/useRealtimeData.ts: PriceMsg.asset and Handlers.onPrice widened to string (BUG-08 expanded the WS price stream to all ASSET_MAP entries).
    • store/dashboard.ts: livePrices changed from { BTC; ETH } to Record<string, number | null>; setLivePrice param widened to string.
    • lib/api.ts: getPrices asset param widened to string.
    • components/trades/TradeTable.tsx: hardcoded ASSETS = ['all','BTC','ETH','SOL'] replaced by a dynamic useMemo that builds the asset list from the actual trade set — new perps (TRUMP, BNB, etc.) appear in the filter automatically.
    • DashboardClient.tsx + TradesPageClient.tsx: removed isZh from useEffect deps (compile-time constant; including it restarted effects on every render).
  • Interaction/button bugs sweep (UI-A..G, FIXED 2026-05-29):

    • A OpenPositions.tsx close-modal backdrop now dismisses in 'err' state too (previously stuck — only 'idle' dismissed). State is reset on dismiss so the next open is clean.
    • B confirmCloseTrade distinguishes user-cancelled signature (isUserRejection) from real failure: cancellation closes the modal silently; real errors stay open in 'err' state with the message.
    • C SystemControl.flipAuto claims busy=true before await confirmSign(...). Rapid double-click on the ON/OFF pill no longer slips past the guard. The slot is released if the user cancels the sheet.
    • D BotConfigPanel "Rotate" API-key button resets keyState/keyErr so a previously-failed save's red error doesn't bleed into the new edit.
    • E Paper/Live Switch clears subErr when toggled so a stale subscribe-error doesn't sit next to the new choice.
    • F confirmSign keeps a module-scope _activeCancel; a second call while a sheet is mounted resolves the first promise with false instead of leaving its caller hanging forever. All 9 call sites verified to cleanly handle the false return (setBusy(false) / setLoadState('idle')).
    • G OpenPositions.toggleGrow errors now write to a dedicated growErr state with a 4-second auto-clear, instead of squatting in the panel-header err banner shared with the 15-s poll error.
  • H2 Chart data race (FIXED 2026-06-01): cancelled flag added to the candles useEffect — stale BTC response can no longer overwrite ETH candles when asset is switched mid-fetch.

  • M8 Duplicate React keys (FIXED 2026-06-01): onNewPost now deduplicates by post.id before prepending to the posts list.

  • L2 fittedRef not reset on asset change (FIXED 2026-06-01): Chart view is re-fitted when asset changes, not only on timeframe change.

  • L4 invalid ARIA role (FIXED 2026-06-01): Ticker.tsx role="marquee" replaced with role="region".

  • L5 ws:// mixed-content block (FIXED 2026-06-01): wsContext.tsx now auto-upgrades to wss:// when served over HTTPS, instead of hard-coding ws://.

  • Data-consistency / UI sweep (FIXED 2026-06-09):

    • Overview headline counts (DashboardClient) now come from server-side /posts-paged totals (getPostsPage(1,1,...) for global / truth / macro), not the 80-post first-paint slice. The local slice is only a pre-load fallback. Without this a 1108-post feed showed "80 tracked / —".
    • "Live" chip downgrades to "Delayed" when /api/health/deep reports is_leader === false (follower process runs no background tasks → data is frozen). Keyed ONLY on is_leader, not status:degraded, to avoid flapping on transient single-feed staleness.
    • KOL pages default to a 30d window (was 7d) everywhere — kol/page.tsx SSR, KolPageClient kolDays, and the Overview KOL-intel card. KOL ingestion is daily/sparse so 7d was frequently empty (7d=0 / 30d=196).
    • KOL divergence "Xd ago" uses post_at, not created_at (a backfill set created_at≈now and made old events look fresh).
    • Macro tone is derived from the backend regime_label, not a re-thresholded score (±15 on the client vs ±20 on the server disagreed).
    • Trump page header count follows the active signal filter.
    • Source labels: blog (KOL), sma_reclaim/rsi_reversal/breakout (TradeTable + PostCards), adopted (TradeTable) registered — no more raw technical strings.
    • TradeTable resets source/asset filters that don't apply to the new wallet's trade set (clamp effect), so a stuck filter + vanished breakdown card can't hide the new history.
    • OpenPositions has an in-page "Unlock positions" button (signs view_user) — no forced detour to Settings.
    • BotConfigPanel: overnight schedules (22:0002:00) are allowed (backend already supports wrap-around); readiness reports NOT-ready for a Macro-only wallet with no Telegram bound (since /adopt needs Telegram).
    • Cross-page selection: clicking a Recent-signals row clears selectedDayPosts so the single-post detail actually shows.

Open / deferred (need interface change or DB migration):

  • C3 Signed READ endpoints (allow_replay=True) put ts/sig in URL → access logs. Fix requires changing read endpoints to POST body (interface change + frontend update).
  • H4 KEK derived with single SHA-256, no salt/KDF. Fix requires re-encrypting all HL API keys (DB migration).
  • M5 GET /telegram/{wallet}/status is unauthenticated — exposes chat_id/tg_username.
  • M7 Rate limit bypassable via spoofed XFF — infrastructure-level fix (nginx/Cloudflare).
  • M9 FIXED 2026-06-12: lib/cache.ts keeps a per-key subscribers list — every caller that hits a stale key during an in-flight revalidation registers its onUpdate, and ALL are notified when the fetch resolves (previously only the caller that started the revalidation got fresh data).
  • Funding Reversal tab Breakout Monitor RESOLVED 2026-06-12 (removed): SignalMonitor unmounted from the Funding tab — the backend scanner has been disabled for months (funding_signal._enabled=False, operator-only toggle) so the panel only ever showed "Paused / No signals yet". The backend's 5-min poll job was also unscheduled (backend/app/main.py); the /signal/* API routes remain. To revive: re-add the add_job() AND remount SignalMonitor in MacroVibesPageClient.

How to verify changes locally

# Type check (must pass with 0 errors)
npx tsc --noEmit

# Production build (catches missing pages, stale imports)
rm -rf .next && npx next build

# Dev server — NOTE: this app runs on port 3001 (see package.json:
# `next dev -p 3001` / `next start -p 3001`), NOT the Next.js default 3000.
# Any external doc, script, or healthcheck must target 3001.
npm run dev

# Pages-200 sweep (every locale page returns 200) — port 3001
for p in /en /en/trump /en/macro /en/kol /en/trades /en/posts /en/settings \
         /en/methodology /en/glossary /en/case-studies /en/privacy /en/terms; do
  echo "$p $(curl -so /dev/null -w '%{http_code}' http://localhost:3001$p)"
done

SEO / GEO surface (don't break)

  • app/layout.tsx has JSON-LD schema.org markup (Organization, SoftwareApplication, FAQ). Search engines + AI crawlers read this.
  • public/llms.txt — canonical doc for AI crawlers (Perplexity, ChatGPT browse, Google AI Overviews). Edit BOTH this file AND layout.tsx when renaming product features.
  • app/sitemap.ts — sitemap.xml. Add new routes here.
  • OG imageapp/opengraph-image.tsx dynamically renders the social card. Keep the brand pill list in sync with current features.

Coupling to backend

  • API contract lives in TypeScript types under lib/api.ts (e.g. OpenPositionsResponse, TodayStats, BotTrade). These must match the backend Pydantic models in app/api/*/positions.py etc.
  • When backend adds a field (e.g. BotTrade.released_at), this repo's lib/api.ts types may need updating to surface it in the UI. Currently released trades are FILTERED OUT by the backend's /positions/open — they don't appear here, so no UI for "managed vs released" yet.
  • When backend renames a route (e.g. /btc/page.tsx/macro/page.tsx), search for ALL references (including telegram deep-link map in backend/app/services/telegram.py).

Deploy

Vercel auto-deploys main branch pushes. No special build flags. Env vars in Vercel dashboard:

  • NEXT_PUBLIC_API_URL — backend HTTPS origin
  • NEXT_PUBLIC_SITE_URL — canonical site URL (used for OG, sitemap)
  • NEXT_PUBLIC_WS_URL — WebSocket endpoint (usually wss://api...)

Sibling repo

  • /Users/k/Public/trumpsignal/backend — Python/FastAPI backend, includes Hyperliquid integration, Telegram bot, AI scoring (DeepSeek), all signal scanners, the adoption/release flow. See its AGENTS.md.