379 lines
19 KiB
Markdown
379 lines
19 KiB
Markdown
# 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 CLAUDE.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:
|
||
```tsx
|
||
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 store** — `store/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`:
|
||
|
||
- **Colors** — `oklch()` 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-01–06, 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:00–02: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
|
||
|
||
```bash
|
||
# 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 image** — `app/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 CLAUDE.md.
|