feat: production codebase for Statista (https://statista.steelforesight.ir)

This commit is contained in:
Mr.Gay100 2026-08-26 07:04:12 +00:00
commit a188b48679
141 changed files with 33372 additions and 0 deletions

44
.claude/settings.json Normal file
View File

@ -0,0 +1,44 @@
{
"permissions": {
"allow": [
"Skill(run)",
"Skill(run:*)",
"PowerShell(python -c \"import importlib.util as u; [print\\(m, 'OK' if u.find_spec\\(m\\) else 'MISSING'\\) for m in ['fastapi','uvicorn','yfinance','pandas','websockets']]\")",
"PowerShell($env:PYTHONUNBUFFERED=\"1\"; python -m uvicorn main:app --host 127.0.0.1 --port 8000 --app-dir backend)",
"PowerShell(npm run dev)",
"PowerShell(python -c \"import importlib.util as u; print\\('playwright', 'OK' if u.find_spec\\('playwright'\\) else 'MISSING'\\)\")",
"PowerShell($env:PYTHONUNBUFFERED=\"1\"; $env:PYTHONUTF8=\"1\"; python -m uvicorn main:app --host 127.0.0.1 --port 8000 --app-dir backend)",
"Read(//c/Users/DATIS STAR/.claude/skills/impeccable/**)",
"Read(//c/Users/DATIS STAR/.agents/skills/impeccable/**)",
"PowerShell($env:PYTHONUNBUFFERED=\"1\"; $env:PYTHONUTF8=\"1\"; python -m uvicorn admin_app:app --host 127.0.0.1 --port 8001 --app-dir admin)",
"PowerShell(npx --yes shadcn@latest add \"https://reactbits.dev/r/LaserFlow-JS-CSS.json\" --yes 2>&1)",
"PowerShell(npm install three@^0.167.1 @types/three --silent 2>&1)",
"PowerShell(npm install framer-motion --silent 2>&1)",
"PowerShell(npm install gsap --silent 2>&1)",
"PowerShell(npm install ogl --silent 2>&1)",
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:5174/)",
"Bash(curl -s -o /dev/null -w '%{http_code}' http://localhost:5174/)",
"Bash(echo \"home: $\\(curl -s -o /dev/null -w '%{http_code}' http://localhost:5174/\\)\")",
"Bash(curl -s -o /dev/null -w '%{http_code}' http://localhost:5174/dashboard)",
"Bash(echo \"dashboard: $\\(curl -s -o /dev/null -w '%{http_code}' http://localhost:5174/dashboard\\)\")",
"Bash(curl -s -m 8 \"http://localhost:8000/api/reports\" -o NUL -w \"reports endpoint: %{http_code}\\\\n\")",
"Bash(curl -s -m 10 'http://localhost:8000/api/reports' -o NUL -w '%{http_code}')",
"Bash(echo \"summary: $\\(curl -s -m 10 'http://localhost:8000/api/reports' -o NUL -w '%{http_code}'\\)\")",
"Bash(curl -s -m 10 'http://localhost:8000/api/reports/files?folder=Platts%20-%20Metals%20Daily' -o NUL -w '%{http_code}')",
"Bash(echo \"files: $\\(curl -s -m 10 'http://localhost:8000/api/reports/files?folder=Platts%20-%20Metals%20Daily' -o NUL -w '%{http_code}'\\)\")",
"Bash(curl -s -m 10 \"http://localhost:8000/api/reports\")",
"Bash(curl -s -m 15 -o NUL -w \"status:%{http_code} type:%{content_type} bytes:%{size_download}\\\\n\" \"http://localhost:8000/api/reports/file?folder=Platts%20-%20Metals%20Daily&name=Metals%20Daily%20\\(27022026\\).pdf\")",
"WebFetch(domain:api.coingecko.com)",
"WebFetch(domain:www.coingecko.com)",
"PowerShell(try { \"dashboard: \" + \\(Invoke-WebRequest -Uri 'http://localhost:5174/dashboard' -UseBasicParsing -TimeoutSec 12\\).StatusCode } catch { \"ERR: $\\($_.Exception.Message\\)\" })",
"PowerShell(npm install -g ngrok 2>&1)",
"PowerShell(ngrok http 5174 --log stdout)",
"PowerShell(Stop-Process -Id 149232 -Force -ErrorAction SilentlyContinue)",
"PowerShell(Get-Process ngrok -ErrorAction SilentlyContinue)",
"PowerShell(Stop-Process -Force -ErrorAction SilentlyContinue)",
"PowerShell(\"killed old ngrok\")",
"PowerShell(& \"C:\\\\Users\\\\DATIS STAR\\\\AppData\\\\Roaming\\\\npm\\\\node_modules\\\\ngrok\\\\bin\\\\ngrok.exe\" http 5174 --log stdout)",
"PowerShell(try { \"login: \" + \\(Invoke-WebRequest 'http://localhost:5174/login' -UseBasicParsing -TimeoutSec 12\\).StatusCode } catch { \"ERR $\\($_.Exception.Message\\)\" })"
]
}
}

32
.env.example Normal file
View File

@ -0,0 +1,32 @@
# Backend (FastAPI) environment reference — copy what you need into the runtime env.
# Real values stay out of git. The dashboard seed login "Admin"/"Admin123" is
# created only when SEED_ADMIN=1.
# SQLite path + reports dir (defaults are fine in Docker; a disk is mounted at /data)
DB_PATH=/data/asianmetal.db
REPORTS_DIR=/data/reports
# CORS allow-list — comma-separated origins of the frontend
ALLOWED_ORIGINS=http://localhost:5174,https://web.didvan.com
# Session cookie: COOKIE_SECURE=1 behind HTTPS; COOKIE_SAMESITE=none for cross-domain
COOKIE_SECURE=0
COOKIE_SAMESITE=lax
# Dangerous development-only seed (Admin/Admin123). Explicitly opt in locally.
SEED_ADMIN=0
# Only these immediate proxy networks may supply X-Real-IP/X-Forwarded-For.
TRUSTED_PROXY_CIDRS=127.0.0.1/32,::1/128,172.16.0.0/12
# Admin panel password (admin_app only)
ADMIN_PASSWORD=change-me-strong-secret
# data-source token for the daily metals/steel sync (sync.py)
ASIANMETAL_TOKEN=
# Optional outbound proxy for geo-blocked sources (only needed on Iran-hosted PaaS;
# the Arvan VPS has direct access and does NOT need these)
# HTTP_PROXY=http://host:port
# HTTPS_PROXY=http://host:port
# SCRAPER_PROXY=http://host:port

62
.gitignore vendored Normal file
View File

@ -0,0 +1,62 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
.output
.vinxi
.tanstack/**
.nitro
*.local
# Wrangler / Cloudflare
.wrangler/
.dev.vars
# Secrets / env — NEVER commit (real values stay out of git)
.env
.env.*
*.env
# ...but DO track the documented templates (placeholders only, no secrets)
!.env.example
!**/.env.example
# SSL Certificates & keys
certs/
certs-git/
certbot-www/
*.pem
*.key
*.crt
# Python
__pycache__/
*.pyc
.venv/
venv/
# Local database
*.db
*.sqlite*
# Backup files
*.bak*
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
graphify-out/

View File

@ -0,0 +1,360 @@
# 🎯 Adversarial Architecture Review
### Closing the gaps a checklist can't see — written from the other side of the keyboard
> The previous checklist hardens each **component**. This document hardens the **seams between them** — the trust relationships, the assumptions, the one boundary you forgot. That's where real attacks live. A checklist asks "is this function safe?" An attacker asks "which of these 40 functions did you forget to protect, and what does owning it get me?"
>
> This is written attacker-first: for each gap, *here's how I'd come at it**here's the architectural control that shuts the door*. Every attack is paired with a fix. Nothing here is operational against anyone else's system — it's a threat model of **your** system so you can close it.
---
## 0. The mindset shift
A checklist is a **list of doors to lock**. An architecture review is **assuming I'm already walking your hallways and asking what I can reach**.
Three principles drive everything below:
1. **The client is enemy territory.** Anything that runs in or comes from a browser is something I control completely. Cookies, JWTs, headers, request bodies, the `user_id` field, the price — all forgeable. If a security decision happens in the browser, it doesn't happen.
2. **Defense in depth or it's not defense.** One auth check protecting your data means one forgotten check = breach. Real security is layers where **each layer assumes the one in front of it already failed.**
3. **Assume breach.** I *will* get a foothold somewhere — a leaked key, an XSS, a dependency. The question your architecture answers is: *then what?* How far do I get, how fast do you notice, how quickly can you kill it. Most vibe-coded apps have no answer because they were designed assuming the wall holds.
---
## 1. How I actually approach your system
I don't start by looking for "an XSS." I start with reconnaissance and a map. The questions I ask, in order:
- **What's the stack?** Response headers, JS bundle, `/_next/`, error pages, favicon hashes, DNS records — they tell me you're Next.js on Vercel with Supabase and Stripe before I've sent a single malicious byte. Now I know exactly which default mistakes to test.
- **Where's the data, and can I reach it directly?** If I see a Supabase anon key in your JS (I will), your database is *on the internet*. I don't need to beat your app — I can query Postgres directly and see if RLS is off.
- **Is identity proven or claimed?** I log in, watch the network tab, and look for any request where the server trusts a `user_id`, `role`, `account_id`, or `isAdmin` that *I* supplied. If changing it changes the result, you're done.
- **What costs you money when I press a button?** Every LLM call, SMS, email, image generation, and export is a way for me to drain your wallet for free.
- **What's the blast radius of one foothold?** If I get one key or one bug, do I get everything, or one small thing? This tells me whether you're worth my time.
- **Will you even notice?** No logging, no alerts, no rate limits = I can take my time, brute-force, enumerate, and exfiltrate at my leisure.
**The architectural defense against recon:** give away as little as possible (Section 6), and make the answers to those questions *boring* — data behind a deny-by-default boundary, identity proven server-side, money gated, blast radius contained, and me visible the moment I act.
---
## 2. Map your trust boundaries (the most important diagram you will ever draw)
90% of breaches happen at a **trust boundary** — the line where untrusted input crosses into a zone that trusts it. If you can't draw these lines, you can't defend them. Here is the map of a typical vibe-coded app, with every boundary I attack:
```
┌──────────────────────────────────────────────────────────────────────┐
│ ZONE 0 — FULLY HOSTILE (you control nothing here) │
│ Browsers · mobile apps · curl · bots · my laptop │
│ Every byte from here is attacker-controlled: cookies, JWTs, headers, │
│ request bodies, "user_id", "role", prices, quantities — all forgeable │
└────────────────────────────────┬─────────────────────────────────────┘
│ ◄══ BOUNDARY #1 (the big one)
│ Everything crossing is a LIE until
│ your server re-proves it.
┌────────────────────────────────▼─────────────────────────────────────┐
│ ZONE 1 — EDGE (the first thing you control) │
│ CDN · WAF · DDoS protection · bot management · TLS termination │
│ Job: drop obvious garbage before it reaches — and costs — your code. │
└────────────────────────────────┬─────────────────────────────────────┘
│ ◄══ BOUNDARY #2
┌────────────────────────────────▼─────────────────────────────────────┐
│ ZONE 2 — APPLICATION (your code) │
│ gateway → AUTHN (prove identity) → AUTHZ (prove permission) │
│ → input validation → business logic │
│ Rule: identity is ESTABLISHED here from a verified session — NEVER │
│ read from a field the client supplied. │
└──────────────┬────────────────────────────────┬──────────────────────┘
│ ◄══ BOUNDARY #3 │ ◄══ BOUNDARY #4
│ (to your data) │ (to third parties)
┌──────────────▼──────────────┐ ┌────────────▼─────────────────────┐
│ ZONE 3 — DATA │ │ ZONE 4 — EXTERNAL │
│ Postgres / Supabase (RLS) │ │ Stripe · LLM API · email · S3 │
│ Object storage │ │ Each = a separate trust domain │
│ Job: DENY BY DEFAULT, even │ │ with its own key & blast radius │
│ when the app layer failed. │ │ Inbound (webhooks) must be │
│ │ │ signature-verified. │
└──────────────┬──────────────┘ └──────────────────────────────────┘
│ ◄══ BOUNDARY #5 (the one everyone forgets)
┌──────────────▼────────────────────────────────────────────────────────┐
│ ZONE 5 — SECRETS & CONTROL PLANE │
│ Env vars · secrets manager · cloud IAM · CI/CD · DNS · your GitHub │
│ Compromise here = game over. Smallest zone, highest value, weakest │
│ protection in most vibe-coded projects. │
└────────────────────────────────────────────────────────────────────────┘
```
**Do this for your real system.** List every arrow that crosses a boundary. For each one, write down: *what does the receiving side assume about this input, and what happens if I violate that assumption?* That list is your actual attack surface — far more useful than any generic checklist.
---
## 3. The core architectural sins (these create whole *classes* of bugs)
Individual vulnerabilities are symptoms. These seven design decisions are the disease. Fix the architecture and entire categories of bugs become impossible instead of "remembered."
### Sin #1 — The client is treated as trusted
**How I exploit it:** I open DevTools and edit anything — the price in the cart, the `user_id` in the request, the `role: "user"` in the signup body, the disabled "admin" button. Client-side validation, client-set prices, and hidden-not-protected features all fall in seconds.
**The fix is architectural, not a patch:** The browser is a **rendering layer with zero authority**. Every decision — who you are, what you may do, what something costs, whether an action is allowed — is *re-made server-side* from server-held state. The client's job is to display and to submit intent; the server decides everything that matters.
### Sin #2 — Your database is on the internet and you didn't know
**How I exploit it:** Supabase/Firebase hand the browser a key that talks *directly* to the database. That's not a bug — it's the model. But it means your DB is a public API. I take the anon key from your JS and `curl` your tables directly, bypassing your app entirely. If RLS is off (the silent default risk), I read everything.
**The fix:** Decide consciously which model you're in. Either (a) **RLS makes the public-DB model safe** — deny by default, every table, policies scoped to `auth.uid()` — or (b) you **never expose the anon key** and route all data access through your own backend. Pick one on purpose. The disaster is doing (a)'s exposure with (b)'s assumptions.
### Sin #3 — Identity is claimed, not proven
**How I exploit it:** The single most common vibe-coded flaw. The server reads `user_id` / `account_id` / `org_id` from the URL, query string, or body and trusts it. I change it to yours and read your data (horizontal escalation), or set `role=admin` (vertical escalation).
**The fix:** Identity is **derived exclusively from the authenticated session server-side**, never from client input. The request says *what* to do; the *who* comes from the verified token only. Every resource access then checks `session.user owns/may-access this resource` as a **separate** step from authentication.
### Sin #4 — One layer of defense
**How I exploit it:** If the only thing between me and your data is an `if (loggedIn)` in one route handler, I only need to find the one route where you forgot it — or the one injection that skips it. One miss = total exposure.
**The fix:** **Layer it so my single win isn't enough.** Auth bypass in the app? RLS at the database still returns nothing for the wrong user. SQL injection? The least-privilege DB user can't read the tables it doesn't need or `DROP` anything. XSS slips through? CSP limits what the injected script can do. Each layer assumes the previous one failed (see the stack in Section 4).
### Sin #5 — Flat blast radius
**How I exploit it:** I get *one* foothold — a leaked key, an SSRF, an RCE in a dependency — and because everything runs in one process with one `.env` holding the DB service-role key, the Stripe secret, *and* the OpenAI key, I now own all of it. One door opens the whole building.
**The fix:** **Segment by blast radius.** Least privilege per component: the public web tier doesn't hold the DB admin key; the LLM-calling worker can't read the users table; the service that sends email can't touch payments. Separate keys, separate scopes, separate failure domains. One foothold should yield one small thing.
### Sin #6 — No idempotency or atomicity
**How I exploit it:** I fire 50 simultaneous requests to redeem a one-time coupon, withdraw a balance, or claim limited inventory. Between your "check" and your "use" there's a gap (TOCTOU), so all 50 succeed. Or I replay your Stripe "payment succeeded" webhook ten times and get ten upgrades.
**The fix:** Critical operations are **atomic and idempotent by design** — database constraints and transactions enforce invariants (unique coupon use, non-negative balance), idempotency keys make replays no-ops, optimistic locking handles concurrency. You can't bolt this on per-endpoint; it's how the operation is built.
### Sin #7 — No assume-breach posture
**How I exploit it:** Nothing watches. No rate limits, so I brute-force and enumerate freely. No anomaly alerts, so I exfiltrate 80,000 records overnight and you find out from a customer. No revocation plan, so even after you notice, I'm in for days while you scramble.
**The fix:** Design as if I'm already inside. **Detection** (log security events, alert on anomalies), **containment** (least privilege limits where I can go), and **response** (every secret rotatable in minutes, a kill switch for expensive features, a documented break-glass procedure). Covered in Section 7.
---
## 4. The hardened reference architecture (defense in depth as a stack)
To get from me at the keyboard to your data, I have to defeat **every** layer below. Each one is built assuming the layer in front of it already failed. That's the whole point — redundancy, not a single perfect wall.
```
I (attacker) ──────────────────────────────────────────────────┐
┌────────────────────────────────────────────────────────────┐ │
│ LAYER 1 · EDGE WAF · rate limit · bot mgmt · DDoS │◄┘
│ "Assume malicious traffic reaches me."│
├────────────────────────────────────────────────────────────┤
│ LAYER 2 · TRANSPORT TLS everywhere · HSTS · no mixed │
│ "Assume the network is being tapped." │
├────────────────────────────────────────────────────────────┤
│ LAYER 3 · AUTHN Prove WHO · short sessions · MFA │
│ "Assume a token will get stolen." │
├────────────────────────────────────────────────────────────┤
│ LAYER 4 · AUTHZ Prove you may touch THIS resource │
│ "Assume an auth check was forgotten." │
├────────────────────────────────────────────────────────────┤
│ LAYER 5 · INPUT Validate · parameterize · encode │
│ "Assume every input is an exploit." │
├────────────────────────────────────────────────────────────┤
│ LAYER 6 · DATA RLS · least-priv DB user · deny first │
│ "Assume the query reached me raw." │
├────────────────────────────────────────────────────────────┤
│ LAYER 7 · SECRETS Segmented · least-priv · rotatable │
│ "Assume one secret will leak." │
├────────────────────────────────────────────────────────────┤
│ LAYER 8 · DETECT Logs · anomaly alerts · kill switches │
│ "Assume I got in. See me. Contain me."│
└────────────────────────────────────────────────────────────┘
your data
```
Each layer maps to a concrete owner in a serverless/BaaS stack:
| Layer | Where it lives (Next.js + Supabase + Stripe example) | The job, in one line |
|------|------|------|
| 1 Edge | Cloudflare / Vercel WAF + bot rules | Drop bots, floods, and known-bad before compute is spent |
| 2 Transport | Platform TLS + HSTS header | No plaintext, no downgrade, no mixed content |
| 3 AuthN | Supabase Auth / Clerk / Auth0, short-lived JWT + refresh | Establish identity from a verified session only |
| 4 AuthZ | Your middleware + per-resource ownership checks | Prove permission for *this* object, separately from login |
| 5 Input | Zod/Pydantic validation, parameterized queries, DOMPurify | Treat every input as hostile |
| 6 Data | Postgres RLS + a least-privilege role | Deny by default even if layers 35 were bypassed |
| 7 Secrets | Host env vars / secrets manager, scoped per service | Contain the damage of any single leaked key |
| 8 Detect | Structured logs + alerting (Sentry, Logflare, etc.) | Notice and contain a breach in progress |
**The test of whether you have depth:** pick any one layer, imagine it completely fails, and ask "am I breached?" If the answer is yes for any single layer, you don't have depth there — you have a single point of failure wearing a security costume.
---
## 5. The kill chain — blocked at every stage
This is how an intrusion actually unfolds, start to finish, and the architectural control that breaks the chain at each link. **You only have to break the chain once** to stop the attack — but breaking it in multiple places is what "defense in depth" means in practice.
### Stage 1 · Reconnaissance
- **What I do:** Fingerprint your stack, enumerate endpoints, read error messages, grab source maps, check `/.git/`, harvest the anon key from your JS, run GraphQL introspection, look for predictable IDs.
- **What stops me:** Minimize disclosure (Section 6). Generic errors, no source maps in prod, no `/.git/` exposure, introspection disabled, UUIDs not sequential IDs, no stack/version leakage. Make recon return nothing useful.
### Stage 2 · Initial access / unauthenticated probing
- **What I do:** Hit your API directly with no auth, query your DB with the anon key, spray credential-stuffing lists at login, look for any endpoint that responds with data before checking who I am.
- **What stops me:** Edge WAF + bot management absorbs the spray. Auth boundary returns 401 before any handler logic. RLS returns empty to the anon key. Rate limiting throttles credential stuffing to uselessness.
### Stage 3 · Authentication bypass / session theft
- **What I do:** Steal a JWT via XSS, replay a non-expiring token, reuse a session that "logged out" only client-side, forge a weakly-signed token.
- **What stops me:** Short-lived access tokens + server-side refresh/revocation. Sessions invalidated server-side on logout. `httpOnly` cookies so XSS can't read them. Strong signing secrets. Re-authentication required for sensitive actions (changing email, payment, deleting account).
### Stage 4 · Authorization bypass (IDOR / privilege escalation) — *the money shot*
- **What I do:** Change a resource ID to yours and read your data. Set `role=admin` or `is_verified=true` via mass assignment. Hit an admin route with a normal session.
- **What stops me:** Identity from the session only (never client input). A separate ownership check on every resource (read *and* write) returning 403. Field whitelisting (no mass assignment). Admin checks server-side on every admin route. **And** — because of depth — RLS at the data layer denies cross-user access even if the app check was forgotten.
### Stage 5 · Injection
- **What I do:** SQL injection via concatenated queries, command injection via `exec`, NoSQL operator injection, SSTI, path traversal, XXE.
- **What stops me:** Parameterized queries everywhere (no concatenation). No user input into shell/eval. Validation at layer 5. WAF catches common payloads at the edge. **And** the least-privilege DB user means even successful injection can't read tables it doesn't need or drop anything — depth again.
### Stage 6 · Lateral movement / privilege expansion
- **What I do:** From one foothold, pivot — use the leaked key to reach other services, read other secrets from the shared env, move from the web tier into the data tier, abuse an over-permissioned service account.
- **What stops me:** **Segmentation and least privilege.** Each component holds only the keys it needs, scoped to only what it needs. The LLM worker can't read users; the email service can't touch payments; the web tier doesn't have the DB admin key. My one foothold is a dead end, not a hallway.
### Stage 7 · Exfiltration
- **What I do:** Pull every record via a list endpoint, walk sequential IDs, scrape the whole site, export bulk data, siphon it out slowly to stay under the radar.
- **What stops me:** No endpoint returns "all rows" — pagination caps + per-account volume limits. RLS scopes every read to the user. Anomaly detection flags one client pulling 50,000 records. Egress monitoring. Scraping patterns (sequential walking, high-volume single client) trigger throttling and alerts.
### Stage 8 · Denial of wallet (the modern DoS)
- **What I do:** Spam your LLM agent, trigger thousands of SMS/emails, force expensive image generation, blow up your cloud egress — not to take you down, but to *bankrupt* you.
- **What stops me:** Every paid downstream action is behind a **quota + circuit breaker + hard spend cap** (Section 8). Per-user limits on agent calls, max tokens, daily budgets, and a kill switch that stops calling the expensive service when a threshold trips.
### Stage 9 · Persistence
- **What I do:** Create a hidden admin account, plant a malicious webhook, add an API key, inject a stored payload that runs later (e.g. a poisoned prompt your agent re-executes, or stored XSS), drop an SSH key.
- **What stops me:** Audit logging of every privilege-changing action with alerts (new admin, new key, new webhook → you get pinged). Immutable audit trail. Stored content treated as untrusted on read. Periodic review of accounts, keys, and webhooks.
### Stage 10 · Covering tracks
- **What I do:** Delete or edit logs to hide what I did.
- **What stops me:** Logs shipped **off-box** to an append-only/immutable store I can't reach from the app tier. If the logging sink is in a different trust zone than the thing being logged, I can't clean up after myself.
---
## 6. The seams a checklist misses (integration trust)
Checklists audit boxes. I attack the **arrows between boxes** — every place one component trusts another. These are the highest-value, least-audited targets.
- **Webhook → your DB (Stripe, etc.):** The arrow assumes "this really came from Stripe." I send a forged `payment.succeeded` with `curl`. *Close it:* verify the signature on every webhook (reject 400 on bad/missing), enforce idempotency on event IDs, derive entitlements from verified events only — never a client redirect.
- **AI agent → tools → your data:** The arrow assumes the model's tool calls are safe. I use prompt injection (direct, or hidden in a document the agent reads) to make it call a tool on *your* data or take a destructive action. *Close it:* scope every tool to the current user with least privilege, validate model-generated tool inputs like any user input, require confirmation for destructive actions, sandbox any code execution, and keep authorization in your code — never in the prompt.
- **Upload → storage → served back:** The arrow assumes an uploaded "image" is an image. I upload a polyglot/HTML file and get it served from your origin to run as same-origin script. *Close it:* validate by magic bytes, rename to UUID, re-encode images, and serve from a **separate origin/bucket** so nothing user-uploaded executes in your security context.
- **CDN cache → private data:** The arrow assumes responses are safe to cache. You cache an authenticated response and the CDN serves User A's private data to User B. *Close it:* explicit `Cache-Control: private/no-store` on all authenticated responses, `Vary` correctly, never cache anything keyed to a user at a shared layer.
- **Service → service (internal calls):** The arrow assumes internal callers are trusted because they're "internal." I find an SSRF or get a foothold and call your internal endpoints, which trust a header like `X-User-Id`. *Close it:* internal services authenticate each other (signed requests / mTLS / a shared secret), and **never** trust client-or-peer-supplied identity headers without verification. There is no "internal therefore trusted."
- **SSO / OAuth callback:** The arrow assumes the callback is legitimate. I tamper with `redirect_uri`, skip the `state` check (CSRF), or replay a code. *Close it:* strict `redirect_uri` allowlist, mandatory `state` parameter, PKCE, single-use codes, validate the token audience.
- **DNS / subdomain → hosting:** The arrow assumes your DNS records point where you think. You delete a Vercel/S3/Heroku app but leave the `CNAME`; I claim the dangling target and now host phishing on *your* subdomain. *Close it:* audit DNS for dangling records, remove records when you tear down resources, registrar lock, CAA records, MFA on your DNS provider.
- **WebSocket / real-time:** The arrow assumes the socket is authed like your HTTP routes. It usually isn't — WS connections frequently skip authn/authz entirely. *Close it:* authenticate on connect, authorize every message, rate-limit per connection, scope subscriptions to the user.
- **Email / notifications → the world:** The arrow assumes your send features are used normally. I abuse "invite a friend" or password-reset to email-bomb a victim, or inject headers via a contact form. *Close it:* rate-limit and cap send features per user, validate/encode anything that reaches an email header, and configure SPF/DKIM/DMARC so no one can send *as* your domain.
- **Third-party scripts → your page:** The arrow assumes your analytics/chat/widget vendors are safe. Their compromise becomes yours (supply-chain via the browser). *Close it:* Subresource Integrity on pinned scripts, a strict CSP, minimize third-party JS, and treat every embedded vendor as code running with your users' trust.
---
## 7. Assume breach — blast-radius containment & response
Everything above tries to keep me out. This section accepts that **someday I get in** and asks the only question that matters then: *how bad is it, and how fast can you end it?* This is what separates an app that has "a breach" from one that has "a catastrophe."
**Contain the blast radius (limit where one foothold reaches):**
- [ ] **Secret segmentation.** No single `.env` holds the keys to everything. The DB service-role key, the Stripe secret, and the LLM key live in *different* trust zones, used by *different* components. One leak ≠ total compromise.
- [ ] **Least privilege, everywhere.** Every key, role, and service account can touch only what it needs. Read-only where possible. No `AdministratorAccess` on app roles. The DB user your app connects with can't drop tables or read tables it doesn't use.
- [ ] **Short-lived credentials.** Prefer rotating/temporary tokens over long-lived static keys. A stolen credential that expires in an hour is worth far less than one that lives forever.
- [ ] **Isolation between tenants/users.** One user's compromise (or one user *being* me) can't reach another's data — enforced at the data layer (RLS), not just the app.
**Detect me (assume I'm acting right now):**
- [ ] **Log security events off-box:** auth failures, 403s, rate-limit hits, admin actions, key/webhook creation, spend spikes — shipped to a store the app tier can't edit.
- [ ] **Alert on anomalies:** 100× traffic spike, logins from new geographies, bulk data access, spend crossing a threshold, a new admin or API key appearing. The alert should reach a human (or a kill switch) in minutes, not days.
- [ ] **Baseline normal**, so abnormal is visible. You can't detect "weird" without knowing "normal."
**End it (have the plan before you need it):**
- [ ] **Rotate everything in minutes.** A documented, tested procedure to roll every secret — DB creds, API keys, signing secrets, OAuth secrets. If rotating your DB password would take you a panicked afternoon, you've already lost.
- [ ] **Kill switches.** Feature flags / env toggles that instantly disable expensive or risky surfaces (the AI agent, file uploads, a leaking endpoint) without a redeploy.
- [ ] **Break-glass runbook.** Written steps: how to take the app offline, revoke sessions, rotate keys, preserve evidence (don't destroy the logs you'll need), and notify users. Decide this calm, not mid-incident.
- [ ] **Backups that survive ransomware.** Immutable/offline copies, and a restore you've **actually tested**. A backup you've never restored is a wish.
---
## 8. The economic attack surface (denial of wallet)
A modern attacker doesn't always want to steal your data or take you down — sometimes they just want to **make your bill explode**, for lulz or extortion. Vibe-coded apps are uniquely exposed because they wire up metered services (LLMs, SMS, email, image gen, egress) with no economic guardrails.
**The design principle:** inventory **every action a user can trigger that costs *you* money**, and gate each one. If pressing a button calls a paid API, an attacker pressing it 100,000 times is a denial-of-wallet attack.
- [ ] **Map the cost surface.** Make a literal list: LLM calls, embeddings, SMS, email, image/video generation, third-party API calls, cloud egress, heavy compute jobs. Each line is an attack vector.
- [ ] **Per-user quotas + rate limits** on every one of them. One user cannot consume the whole budget.
- [ ] **Hard spend caps** at the provider (OpenAI/Anthropic/Twilio billing limits) *and* a server-side **circuit breaker** that stops calling out when a daily/hourly threshold trips.
- [ ] **Bounded inputs/outputs:** max input length, max output tokens, max file size, max job duration — so a single request can't be made arbitrarily expensive.
- [ ] **Anomaly alerts on spend**, so a 50× cost spike pages you within minutes instead of arriving as a month-end invoice.
- [ ] **Decompression/zip-bomb and complexity limits** so one cheap request can't trigger ruinous server work.
---
## 9. Atomicity, idempotency & race conditions (business logic at the architecture level)
This is the category AI tools and checklists are *worst* at, because it's about *your* rules and *concurrent* behavior — invisible when you read code top-to-bottom. It's how I get free money without "hacking" anything.
**Race conditions / TOCTOU (time-of-check to time-of-use):**
- **How I exploit it:** I send many requests *simultaneously* so they all pass the "check" before any of them "uses." Redeem a one-time coupon 50 times. Withdraw a $100 balance 50 times. Claim the last 1 inventory item 50 times. Refer-and-reward myself in a loop.
- **The fix is at the data layer, not the handler:** Enforce invariants with **database constraints and transactions** (unique constraint on coupon-use, `CHECK balance >= 0`, atomic decrement with row locking). Optimistic or pessimistic locking on contended rows. The rule can't live in app-level "check then write" — that gap is the vulnerability.
**Idempotency / replay:**
- **How I exploit it:** I replay the same request — a payment confirmation, a webhook, a "claim reward" — and get the effect twice (or N times). Network retries can even trigger this by accident.
- **The fix:** **Idempotency keys** on state-changing operations (the same key = the same single effect, no matter how many times it arrives). Signed + timestamped + nonce'd requests to reject replays. Stored processed-event IDs for webhooks.
**Workflow / state-machine bypass:**
- **How I exploit it:** I skip steps — reach a paid feature without completing payment, hit "confirm" without "review," jump to a later state by calling its endpoint directly.
- **The fix:** Enforce state transitions **server-side** as an explicit state machine; each step verifies the prior state actually holds. Don't assume users walk your happy path — I won't.
**Value/quantity tampering:**
- **How I exploit it:** Negative quantities to get a credit, fractional cents and rounding abuse, currency confusion, a price field I supplied.
- **The fix:** Server-side validation of ranges and signs; prices and totals computed server-side from trusted data; decimal/money types, never floats for currency.
---
## 10. If I were attacking you tomorrow — my first 12 moves
A concrete red-team walk against a typical vibe-coded stack (Next.js + Supabase + Stripe + an LLM agent). For each: what I try, why it usually works, and the architectural control that turns it into a dead end. *(Patterns only — this is your threat model, not a weapon against others.)*
1. **Fingerprint the stack** from headers/bundle/DNS. → *Dead end:* nothing sensitive in headers, no source maps, generic errors. I learn nothing.
2. **Pull the Supabase anon key** from your JS and `curl` your tables directly. → *Dead end:* RLS denies by default; I get empty arrays. (This one move breaks ~half of vibe-coded apps. Make sure it breaks on yours.)
3. **Map every API route** and hit each one **unauthenticated**, looking for one that returns data. → *Dead end:* 401 before any handler logic, on every protected route.
4. **Log in and tamper with identity** — change `user_id`/`org_id` in requests, flip `role` in the signup body. → *Dead end:* identity comes from the session only; ownership checked per resource; field whitelisting blocks role injection; RLS backstops it.
5. **Walk resource IDs** (`/orders/1001`, `1002`, …) to read others' data. → *Dead end:* UUIDs + ownership checks + RLS; enumeration yields nothing.
6. **Credential-stuff the login** with a breach list. → *Dead end:* rate limiting + bot management + MFA make it uneconomical.
7. **Hammer the AI agent** with a request flood and oversized prompts to spike your LLM bill, and try **prompt injection** to make it leak data or call a tool. → *Dead end:* per-user quotas + token caps + spend circuit breaker; tools scoped to me with least privilege; injection can't change authorization because that lives in your code.
8. **Forge a Stripe webhook** (`payment.succeeded`) to grant myself a paid plan, then **replay** a real one for extra. → *Dead end:* signature verification rejects the forgery; idempotency rejects the replay.
9. **Race a one-time action** — coupon, balance, inventory — with 50 concurrent requests. → *Dead end:* DB constraints + atomic transactions; only one succeeds.
10. **Upload a disguised file** (HTML/script named `.jpg`) and load it from your origin. → *Dead end:* magic-byte validation + re-encode + served from a separate bucket; nothing runs in your context.
11. **Probe the seams** — SSRF to `169.254.169.254` for cloud creds, internal endpoints trusting `X-User-Id`, a dangling DNS subdomain. → *Dead end:* SSRF blocks private IPs; internal calls are authenticated; DNS has no dangling records.
12. **Exfiltrate in bulk** via a list endpoint or slow scrape. → *Dead end:* pagination caps + per-account volume limits + anomaly alerts; I get flagged and throttled long before I get your dataset.
**If even half of these are dead ends on your system, you're already past the bar that took down every app on the breach lists.** The ones that aren't dead ends yet — fix those first; they're your real exposure.
---
## 11. Architecture Decision Records (commit these — they're load-bearing)
Turn the principles above into **binding decisions** you write down and don't relitigate. When your AI agent (or future you) is about to violate one, the ADR is what stops it.
- **AD-1 · The client has zero authority.** Every security decision is re-made server-side from server-held state. The browser displays and submits intent; it decides nothing.
- **AD-2 · The data layer denies by default.** RLS on every table, scoped to `auth.uid()`. The database protects itself even if the app layer is wrong.
- **AD-3 · Identity is proven, never claimed.** The authenticated user comes from the verified session only — never from a client-supplied field.
- **AD-4 · Authorization is separate from authentication.** Every resource access independently verifies the user may touch *this* object, on reads and writes.
- **AD-5 · Secrets are segmented by blast radius.** No component holds a key it doesn't use; one leak compromises one thing, not everything.
- **AD-6 · Every metered action is gated.** Quota + rate limit + spend cap + circuit breaker on anything that costs money.
- **AD-7 · State-changing operations are atomic and idempotent.** Invariants enforced at the data layer; replays are no-ops.
- **AD-8 · Trust boundaries are explicit and authenticated.** No "internal therefore trusted." Every cross-boundary call verifies the caller; every inbound webhook verifies its signature.
- **AD-9 · Assume breach.** Log off-box, alert on anomalies, and keep every secret rotatable in minutes with a kill switch and a runbook.
- **AD-10 · Minimize disclosure.** Generic errors, no source maps, no introspection in prod, non-sequential IDs, nothing in recon that helps an attacker.
---
## 12. How I'd verify these actually hold (red-team test plan)
Architecture controls are worthless if they're aspirational. Prove each boundary holds with an adversarial test — not "does the feature work" but "does the *failure mode* fail closed":
- [ ] **Boundary #1 (client→server):** tamper with every client-supplied field (`user_id`, `role`, price, quantity) and confirm the server ignores or rejects it.
- [ ] **Boundary #3 (app→data):** query the DB with only the anon key; confirm RLS returns nothing for data you shouldn't see.
- [ ] **Depth check:** disable the app-layer auth check in a test and confirm RLS *still* blocks cross-user reads. (If it doesn't, you have one layer, not two.)
- [ ] **AuthZ:** with User A's session, attempt to read and write User B's resources on every endpoint that takes an ID → all 403.
- [ ] **Seams:** send an unsigned webhook (→400), replay a signed one (→ no double effect), try SSRF to internal IPs (→ blocked), upload a disguised file (→ can't execute on your origin).
- [ ] **Race:** fire concurrent requests at every one-time/limited action and confirm exactly one succeeds.
- [ ] **Denial of wallet:** burst the agent and metered endpoints; confirm quotas, caps, and the circuit breaker trip.
- [ ] **Blast radius:** assume one key leaked — trace on paper exactly what it reaches. If "everything," fix the segmentation.
- [ ] **Detection:** run the above and check that your logging/alerting actually *noticed*. Silent controls you can't observe failing are controls you can't trust.
When you've turned all of these into dead ends, **bring in a human pentester** to find the things a threat model can't — business-logic flaws specific to your domain, chained low-severity bugs, and the creative paths neither of us listed. That's the one step no document replaces.
---
## TL;DR — the architecture in one breath
> The client is hostile and decides nothing. Identity is proven server-side, never claimed. Every resource access is authorized separately, on a data layer that denies by default — so one forgotten check isn't a breach. Secrets are segmented so one leak isn't game over. Every action that costs money is gated. State changes are atomic and idempotent. Every trust boundary is explicit and authenticated. And you assume I'm already inside — so you log, alert, contain, and can rotate everything in minutes. **Make my first twelve moves dead ends, then hire someone to find the thirteenth.**
---
*Companion to the SECURITY-CHECKLIST. The checklist hardens components; this hardens the architecture and the seams between them. Neither makes you "unhackable" — together they make you a hard, contained, well-monitored target, which is the real goal. When you have real users and revenue, a human penetration test is the next step, not an optional one.*

532
API-DOCUMENTATION-FA.md Normal file
View File

@ -0,0 +1,532 @@
# مستندات API قیمت ها
این API برای دریافت قیمت ها و داده های بازار به صورت JSON ساخته شده است.
تمام endpoint های اشتراکی با `/api/v1` شروع می شوند و فقط خواندنی هستند.
## 1. آدرس پایه
```txt
https://statista.steelforesight.ir
```
## 2. احراز هویت
برای تمام درخواست ها باید هدر زیر ارسال شود:
```http
Authorization: Bearer YOUR_API_TOKEN
```
اگر این هدر ارسال نشود یا توکن اشتباه باشد، پاسخ `401` برمی گردد.
نمونه:
```js
const TOKEN = "YOUR_API_TOKEN";
const res = await fetch("https://statista.steelforesight.ir/api/v1/prices/latest", {
headers: {
Authorization: `Bearer ${TOKEN}`,
},
});
const data = await res.json();
```
## 3. دریافت همه داده ها در یک درخواست
```http
GET /api/v1/prices/latest
```
آدرس کامل:
```txt
https://statista.steelforesight.ir/api/v1/prices/latest
```
این endpoint همه دیتاست ها را در یک پاسخ JSON برمی گرداند.
نمونه ساختار پاسخ:
```json
{
"generated_at": "2026-07-04T10:50:00Z",
"datasets": {
"metals": [],
"live_commodities": [],
"currency": [],
"gold": [],
"coin": [],
"commodity": [],
"steel_stocks": [],
"crypto": {},
"world_economy": []
}
}
```
معنی هر کلید:
| کلید | منبع | توضیح |
|---|---|---|
| `metals` | دیتابیس داخلی | محصولات فلزی و فولادی |
| `currency` | TGJU | ارز |
| `gold` | TGJU | طلا و نقره |
| `coin` | TGJU | سکه |
| `commodity` | Yahoo Finance | کامودیتی های جهانی مثل نفت، طلا، مس و غیره |
| `steel_stocks` | CompaniesMarketCap | شرکت های فولادی جهان |
| `crypto` | CoinGecko | کریپتو، مارکت جهانی و ترندها |
| `world_economy` | Trading Economics | شاخص های اقتصاد کلان کشورها |
| `live_commodities` | داخلی/لایو | دیتاست کامودیتی زنده، اگر فعال باشد |
## 4. محصولات فلزی و فولادی
### 4.1 فهرست کامل گروه ها، دسته ها و محصولات
```http
GET /api/v1/metals/tree
```
آدرس کامل:
```txt
https://statista.steelforesight.ir/api/v1/metals/tree
```
نمونه پاسخ:
```json
{
"Minor Metals": {
"Lithium": [
"Lithium Carbonate 99.5%min China",
"Lithium Hydroxide 56.5%min China"
],
"Cobalt": [
"Cobalt Metal 99.8%min China"
]
},
"Carbon Steel": {
"Rebar": [
"Rebar HRB400 20mm China"
]
}
}
```
تعداد محصولات فلزی موجود در کاتالوگ داخلی:
```txt
674 نام محصول یکتا
686 رکورد در گروه/دسته، چون بعضی محصولات در بیش از یک دسته دیده می شوند
```
### 4.2 آخرین قیمت همه محصولات فلزی
```http
GET /api/v1/metals/latest
```
آدرس کامل:
```txt
https://statista.steelforesight.ir/api/v1/metals/latest
```
نمونه پاسخ:
```json
[
{
"group": "Minor Metals",
"category": "Lithium",
"title": "Lithium Carbonate 99.5%min China",
"date": "2026-07-04",
"low": 72000,
"mid": 73500,
"high": 75000,
"prev_mid": 73000,
"pct": 0.68
}
]
```
توضیح فیلدها:
| فیلد | توضیح |
|---|---|
| `group` | گروه اصلی محصول |
| `category` | دسته محصول |
| `title` | نام محصول |
| `date` | تاریخ آخرین قیمت |
| `low` | کمترین قیمت |
| `mid` | قیمت میانی |
| `high` | بیشترین قیمت |
| `prev_mid` | قیمت میانی قبلی |
| `pct` | درصد تغییر نسبت به قیمت قبلی |
### 4.3 تاریخچه قیمت یک محصول
```http
GET /api/v1/metals/history?title=PRODUCT_TITLE&days=365
```
پارامترها:
| پارامتر | توضیح |
|---|---|
| `title` | نام دقیق محصول |
| `days` | تعداد روزهای تاریخچه. اگر `0` باشد، همه تاریخچه برمی گردد |
نمونه پاسخ:
```json
[
{
"date": "2026-07-01",
"low": 71000,
"mid": 72500,
"high": 74000
}
]
```
### 4.4 جستجوی محصول
```http
GET /api/v1/metals/search?q=lithium
```
نمونه پاسخ:
```json
[
{
"title": "Lithium Carbonate 99.5%min China",
"group": "Minor Metals",
"category": "Lithium"
}
]
```
## 5. TGJU: ارز، طلا و سکه
داده های TGJU در سه endpoint جدا ارائه می شوند.
### 5.1 ارز
```http
GET /api/v1/currency
```
آدرس کامل:
```txt
https://statista.steelforesight.ir/api/v1/currency
```
### 5.2 طلا
```http
GET /api/v1/gold
```
آدرس کامل:
```txt
https://statista.steelforesight.ir/api/v1/gold
```
### 5.3 سکه
```http
GET /api/v1/coin
```
آدرس کامل:
```txt
https://statista.steelforesight.ir/api/v1/coin
```
نمونه پاسخ TGJU:
```json
[
{
"name": "دلار / نقدی",
"price": 610000,
"change_val": "(0%) 0",
"pct": 0,
"dir": "up",
"fetched_at": "2026-07-04T10:41:25Z"
}
]
```
توضیح فیلدها:
| فیلد | توضیح |
|---|---|
| `name` | نام آیتم |
| `price` | قیمت عددی |
| `change_val` | متن تغییر قیمت طبق منبع |
| `pct` | درصد تغییر |
| `dir` | جهت تغییر، مثل `up` یا `down` |
| `fetched_at` | زمان دریافت داده |
در endpoint کلی:
```js
data.datasets.currency
data.datasets.gold
data.datasets.coin
```
## 6. CoinGecko: کریپتو
```http
GET /api/v1/crypto
```
آدرس کامل:
```txt
https://statista.steelforesight.ir/api/v1/crypto
```
نمونه پاسخ:
```json
{
"coins": [
{
"id": "bitcoin",
"symbol": "BTC",
"name": "Bitcoin",
"rank": 1,
"price": 65000,
"pct_1h": 0.1,
"pct_24h": 1.2,
"pct_7d": -3.4,
"market_cap": 1280000000000,
"volume_24h": 32000000000,
"high_24h": 66000,
"low_24h": 64000,
"sparkline": [],
"dir": "up"
}
],
"global": {
"market_cap_usd": 2500000000000,
"volume_usd": 90000000000,
"btc_dominance": 51.2,
"eth_dominance": 17.4
},
"trending": [],
"fetched_at": "2026-07-04T10:41:25Z"
}
```
در endpoint کلی:
```js
data.datasets.crypto
```
## 7. CompaniesMarketCap: سهام شرکت های فولادی
```http
GET /api/v1/steel-stocks
```
آدرس کامل:
```txt
https://statista.steelforesight.ir/api/v1/steel-stocks
```
نمونه پاسخ:
```json
[
{
"code": "NUE",
"rank": 1,
"name": "Nucor",
"market_cap": 38.5,
"price": 156.2,
"today_pct": 1.4,
"dir": "up",
"country": "United States",
"fetched_at": "2026-07-04T10:41:25Z"
}
]
```
در endpoint کلی:
```js
data.datasets.steel_stocks
```
## 8. Yahoo Finance: کامودیتی های جهانی
```http
GET /api/v1/commodity
```
آدرس کامل:
```txt
https://statista.steelforesight.ir/api/v1/commodity
```
نمونه پاسخ:
```json
[
{
"symbol": "GC=F",
"name": "Gold",
"price": 2350.5,
"change": 8.2,
"pct": 0.35,
"currency": "USD",
"up": true
}
]
```
در endpoint کلی:
```js
data.datasets.commodity
```
## 9. Trading Economics: اقتصاد جهان
```http
GET /api/v1/world-economy
```
آدرس کامل:
```txt
https://statista.steelforesight.ir/api/v1/world-economy
```
نمونه پاسخ:
```json
[
{
"country": "United States",
"gdp": 27360,
"gdp_growth": 1.4,
"interest_rate": 5.5,
"inflation_rate": 3.2,
"jobless_rate": 4.0,
"gov_budget": -6.3,
"debt_gdp": 122.3,
"current_account": -3.0,
"population": 335,
"fetched_at": "2026-07-04T10:41:25Z"
}
]
```
در endpoint کلی:
```js
data.datasets.world_economy
```
## 10. کشف endpoint های موجود
```http
GET /api/v1/datasets
```
آدرس کامل:
```txt
https://statista.steelforesight.ir/api/v1/datasets
```
این endpoint لیست مسیرهای API را برمی گرداند.
## 11. نمونه کامل JavaScript
```js
const API = "https://statista.steelforesight.ir";
const TOKEN = "YOUR_API_TOKEN";
async function getJson(path) {
const res = await fetch(`${API}${path}`, {
headers: {
Authorization: `Bearer ${TOKEN}`,
},
});
if (!res.ok) {
throw new Error(`API request failed: ${res.status}`);
}
return res.json();
}
const allPrices = await getJson("/api/v1/prices/latest");
console.log(allPrices.datasets.metals);
console.log(allPrices.datasets.currency);
console.log(allPrices.datasets.gold);
console.log(allPrices.datasets.coin);
console.log(allPrices.datasets.crypto);
console.log(allPrices.datasets.steel_stocks);
console.log(allPrices.datasets.commodity);
console.log(allPrices.datasets.world_economy);
```
## 12. نمونه تست با PowerShell
```powershell
$TOKEN = "YOUR_API_TOKEN"
$data = Invoke-RestMethod `
-Uri "https://statista.steelforesight.ir/api/v1/prices/latest" `
-Headers @{ Authorization = "Bearer $TOKEN" }
$data.datasets.currency | Select-Object -First 5
$data.datasets.gold | Select-Object -First 5
$data.datasets.coin | Select-Object -First 5
```
اگر متن فارسی در PowerShell خراب نمایش داده شد، مشکل از encoding ترمینال است. برای بهتر شدن نمایش:
```powershell
chcp 65001
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
```
## 13. وضعیت فعلی دیتاست های Live
طبق آخرین خروجی live API:
```txt
TGJU currency: 33
TGJU gold: 14
TGJU coin: 5
Yahoo Finance commodities: 19
CompaniesMarketCap steel stocks: 81
CoinGecko top crypto: 100
CoinGecko trending: 15
```
کاتالوگ داخلی فلزات:
```txt
674 نام محصول فلزی/فولادی یکتا
```

485
API-SHARE.md Normal file
View File

@ -0,0 +1,485 @@
# مستند API اشتراک قیمت ها
این API برای دریافت خروجی JSON از قیمت ها و داده های بازار استفاده می شود.
همه endpoint های نسخه اشتراکی با `/api/v1` شروع می شوند و فقط خواندنی هستند.
## اطلاعات اصلی
Base URL:
```txt
https://YOUR-DOMAIN.com
```
Header احراز هویت:
```http
Authorization: Bearer YOUR_API_TOKEN
```
توکن در بک اند از متغیر محیطی زیر خوانده می شود:
```txt
API_TOKEN
```
اگر توکن ارسال نشود یا اشتباه باشد، پاسخ `401` برمی گردد.
اگر توکن روی سرور تنظیم نشده باشد، پاسخ `503` برمی گردد.
## دریافت همه قیمت ها در یک درخواست
```http
GET /api/v1/prices/latest
```
این endpoint همه دیتاست های اصلی را در یک JSON واحد برمی گرداند.
نمونه درخواست JavaScript:
```js
const API = "https://YOUR-DOMAIN.com";
const TOKEN = "YOUR_API_TOKEN";
const response = await fetch(`${API}/api/v1/prices/latest`, {
headers: {
Authorization: `Bearer ${TOKEN}`,
},
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
console.log(data.generated_at);
console.log(data.datasets.metals);
console.log(data.datasets.currency);
```
ساختار کلی پاسخ:
```json
{
"generated_at": "2026-07-04T09:30:00Z",
"datasets": {
"metals": [],
"live_commodities": [],
"currency": [],
"gold": [],
"coin": [],
"commodity": [],
"steel_stocks": [],
"crypto": {},
"world_economy": []
}
}
```
## فهرست کامل محصولات فلزی
برای دیدن همه گروه ها، دسته ها و نام محصولات:
```http
GET /api/v1/metals/tree
```
نمونه پاسخ:
```json
{
"Minor Metals": {
"Lithium": [
"Lithium Carbonate 99.5%min China",
"Lithium Hydroxide 56.5%min China"
],
"Cobalt": [
"Cobalt Metal 99.8%min China"
]
},
"Carbon Steel": {
"Rebar": [
"Rebar HRB400 20mm China"
]
}
}
```
تعداد فعلی محصولات فلزی در دیتابیس اصلی: **674 محصول**.
## آخرین قیمت همه محصولات فلزی
```http
GET /api/v1/metals/latest
```
نمونه پاسخ:
```json
[
{
"group": "Minor Metals",
"category": "Lithium",
"title": "Lithium Carbonate 99.5%min China",
"date": "2026-07-04",
"low": 72000,
"mid": 73500,
"high": 75000,
"prev_mid": 73000,
"pct": 0.68
}
]
```
توضیح فیلدها:
| فیلد | توضیح |
|---|---|
| `group` | گروه اصلی محصول |
| `category` | دسته محصول |
| `title` | نام کامل محصول |
| `date` | تاریخ آخرین قیمت |
| `low` | کمترین قیمت |
| `mid` | قیمت میانی |
| `high` | بیشترین قیمت |
| `prev_mid` | قیمت میانی قبلی |
| `pct` | درصد تغییر نسبت به قیمت قبلی |
## تاریخچه قیمت یک محصول
```http
GET /api/v1/metals/history?title=PRODUCT_TITLE&days=365
```
پارامترها:
| پارامتر | توضیح |
|---|---|
| `title` | نام دقیق محصول |
| `days` | تعداد روزهای تاریخچه. اگر `0` باشد، همه تاریخچه برمی گردد |
نمونه پاسخ:
```json
[
{
"date": "2026-07-01",
"low": 71000,
"mid": 72500,
"high": 74000
},
{
"date": "2026-07-02",
"low": 72000,
"mid": 73500,
"high": 75000
}
]
```
## جستجوی محصول فلزی
```http
GET /api/v1/metals/search?q=lithium
```
نمونه پاسخ:
```json
[
{
"title": "Lithium Carbonate 99.5%min China",
"group": "Minor Metals",
"category": "Lithium"
}
]
```
## TGJU: ارز، طلا و سکه
داده های TGJU در سه endpoint جدا ارائه می شوند:
```http
GET /api/v1/currency
GET /api/v1/gold
GET /api/v1/coin
```
همین داده ها داخل endpoint کلی هم وجود دارند:
```js
data.datasets.currency
data.datasets.gold
data.datasets.coin
```
نمونه پاسخ ارز:
```json
[
{
"name": "دلار",
"price": 610000,
"change_val": "1,200 (0.2%)",
"pct": 0.2,
"dir": "up",
"fetched_at": "2026-07-04T09:30:00Z"
}
]
```
نمونه پاسخ طلا:
```json
[
{
"name": "طلای 18 عیار",
"price": 7200000,
"change_val": "25,000 (0.35%)",
"pct": 0.35,
"dir": "up",
"fetched_at": "2026-07-04T09:30:00Z"
}
]
```
نمونه پاسخ سکه:
```json
[
{
"name": "سکه امامی",
"price": 850000000,
"change_val": "-1,500,000 (0.18%)",
"pct": -0.18,
"dir": "down",
"fetched_at": "2026-07-04T09:30:00Z"
}
]
```
توضیح فیلدهای TGJU:
| فیلد | توضیح |
|---|---|
| `name` | نام آیتم |
| `price` | قیمت عددی |
| `change_val` | متن تغییر قیمت طبق منبع |
| `pct` | درصد تغییر |
| `dir` | جهت تغییر: `up` یا `down` |
| `fetched_at` | زمان دریافت داده |
## CoinGecko: کریپتو
```http
GET /api/v1/crypto
```
داخل endpoint کلی:
```js
data.datasets.crypto
```
نمونه پاسخ:
```json
{
"coins": [
{
"id": "bitcoin",
"symbol": "BTC",
"name": "Bitcoin",
"image": "https://...",
"rank": 1,
"price": 65000,
"pct_1h": 0.1,
"pct_24h": 1.2,
"pct_7d": -3.4,
"market_cap": 1280000000000,
"volume_24h": 32000000000,
"high_24h": 66000,
"low_24h": 64000,
"circ_supply": 19600000,
"ath": 69000,
"sparkline": [64200, 64500, 65000],
"dir": "up"
}
],
"global": {
"market_cap_usd": 2500000000000,
"volume_usd": 90000000000,
"btc_dominance": 51.2,
"eth_dominance": 17.4,
"market_cap_change_24h": 1.1,
"active": 12000
},
"trending": [
{
"id": "example",
"symbol": "EXM",
"name": "Example Coin",
"rank": 120,
"thumb": "https://...",
"price": 1.23,
"pct_24h": 4.5
}
],
"fetched_at": "2026-07-04T09:30:00Z"
}
```
## CompaniesMarketCap: سهام شرکت های فولادی
```http
GET /api/v1/steel-stocks
```
داخل endpoint کلی:
```js
data.datasets.steel_stocks
```
نمونه پاسخ:
```json
[
{
"code": "NUE",
"rank": 1,
"name": "Nucor",
"market_cap": 38.5,
"price": 156.2,
"today_pct": 1.4,
"dir": "up",
"country": "United States",
"fetched_at": "2026-07-04T09:30:00Z"
}
]
```
توضیح:
| فیلد | توضیح |
|---|---|
| `code` | نماد شرکت |
| `rank` | رتبه شرکت در لیست منبع |
| `name` | نام شرکت |
| `market_cap` | ارزش بازار، بر حسب میلیارد دلار |
| `price` | قیمت سهم |
| `today_pct` | درصد تغییر امروز |
| `dir` | جهت تغییر |
| `country` | کشور شرکت |
| `fetched_at` | زمان دریافت داده |
## Yahoo Finance: کامودیتی های جهانی
```http
GET /api/v1/commodity
```
داخل endpoint کلی:
```js
data.datasets.commodity
```
نمونه پاسخ:
```json
[
{
"symbol": "GC=F",
"name": "Gold",
"price": 2350.5,
"change": 8.2,
"pct": 0.35,
"currency": "USD",
"up": true
}
]
```
## Trading Economics: اقتصاد جهان
```http
GET /api/v1/world-economy
```
داخل endpoint کلی:
```js
data.datasets.world_economy
```
نمونه پاسخ:
```json
[
{
"country": "United States",
"gdp": 27360,
"gdp_growth": 1.4,
"interest_rate": 5.5,
"inflation_rate": 3.2,
"jobless_rate": 4.0,
"gov_budget": -6.3,
"debt_gdp": 122.3,
"current_account": -3.0,
"population": 335,
"fetched_at": "2026-07-04T09:30:00Z"
}
]
```
## کشف endpoint ها
برای گرفتن لیست endpoint های موجود:
```http
GET /api/v1/datasets
```
## نمونه کامل برای دوست شما
```js
const API = "https://YOUR-DOMAIN.com";
const TOKEN = "YOUR_API_TOKEN";
async function getJson(path) {
const res = await fetch(`${API}${path}`, {
headers: {
Authorization: `Bearer ${TOKEN}`,
},
});
if (!res.ok) {
throw new Error(`API request failed: ${res.status}`);
}
return res.json();
}
const catalog = await getJson("/api/v1/metals/tree");
const latestMetals = await getJson("/api/v1/metals/latest");
const allPrices = await getJson("/api/v1/prices/latest");
console.log("Catalog:", catalog);
console.log("Latest metals:", latestMetals);
console.log("TGJU currency:", allPrices.datasets.currency);
console.log("Crypto:", allPrices.datasets.crypto);
```
## خلاصه دیتاست ها
| کلید در JSON | منبع | توضیح |
|---|---|---|
| `metals` | دیتابیس داخلی فلزات | 674 محصول فلزی و فولادی |
| `currency` | TGJU | ارز |
| `gold` | TGJU | طلا |
| `coin` | TGJU | سکه |
| `commodity` | Yahoo Finance | کامودیتی های جهانی مثل نفت، طلا، مس و غیره |
| `steel_stocks` | CompaniesMarketCap | شرکت های فولادی برتر جهان |
| `crypto` | CoinGecko | کریپتو، مارکت جهانی و ترندها |
| `world_economy` | Trading Economics | شاخص های اقتصاد کلان کشورها |

98
DEPLOY-ARVAN.md Normal file
View File

@ -0,0 +1,98 @@
# Arvan VPS — Deploy Guide
Battle-tested steps for deploying to the Arvan cloud server. Written after a
deploy where we hit every trap below — follow this and they won't bite again.
## Server facts
| Thing | Value |
|---|---|
| SSH host (inbound) | `94.101.187.12` |
| **Egress IP** (outbound — for API whitelists, e.g. Asian Metal) | **`31.171.101.234`** ⚠️ different from SSH IP |
| SSH user | `root` **only** (key auth) — `ubuntu` does NOT work |
| SSH key | `~/.ssh/yz_deploy_ed25519` (public key is in `root`'s `authorized_keys`) |
| Code on server | `/root/statista`**plain copied tree, NOT a git repo** |
| Live config | `/root/statista/deploy/.env` (root-only, **untracked — never in git**) |
| Stack | `docker compose -f deploy/docker-compose.yml``nginx` + `frontend` (:3000) + `backend` (:8000) |
| Public URL | `http://94.101.187.12` (nginx on :80) |
## The deploy (copy-paste, run from your local repo)
```bash
KEY=~/.ssh/yz_deploy_ed25519
BR=deploy-vps-restructure # branch to deploy
# 1) ship the committed tree (NOT a git pull — server isn't a git repo).
# git archive only includes tracked files, so deploy/.env + data are untouched.
git archive --format=tar "$BR" | \
ssh -i "$KEY" root@94.101.187.12 'cd /root/statista && tar -xf -'
# 2) rebuild images + recreate containers
ssh -i "$KEY" root@94.101.187.12 \
'cd /root/statista && docker compose -f deploy/docker-compose.yml up -d --build'
# 3) ⚠️ ALWAYS restart nginx afterwards (see Trap #2)
ssh -i "$KEY" root@94.101.187.12 \
'cd /root/statista && docker compose -f deploy/docker-compose.yml restart nginx'
# 4) verify (all must be 200)
ssh -i "$KEY" root@94.101.187.12 \
'for p in / /manifest.webmanifest /sw.js /api/currency; do
echo "$p -> $(curl -s -o /dev/null -w "%{http_code}" http://localhost$p)"; done'
```
Commit + push first (`git push origin deploy-vps-restructure`) if you want the
GitHub copy current — but the archive deploys your **local** committed tree
regardless, so step 1 is the source of truth for the server.
## Traps we hit (and the fixes)
### Trap #1 — Server is NOT a git repo
`/root/statista` was copied (rsync/scp), not cloned. `git pull` fails with
"not a git repository". **Fix:** deploy with `git archive | ssh … tar -xf -`
(above). It overwrites only tracked files; untracked `deploy/.env`, the SQLite
DB, and `/data` reports are left alone.
### Trap #2 — nginx 502 after rebuild (stale upstream IP)
`up -d --build` recreates `frontend`/`backend` with **new Docker IPs**, but
nginx keeps running and caches the **old** IPs → every route returns **502**
even though the containers are healthy and listening. **Fix:** always
`docker compose restart nginx` as the last step. (Confirmed: frontend logs show
`Listening on :3000` while nginx still 502s — that's this, not an app bug.)
### Trap #3 — SSH access
- Only **`root` + key** works. `ubuntu` is rejected.
- **Password login is unreliable via Arvan's web console** — the noVNC keyboard
layout mistypes mixed-case/symbol passwords, so a *correct* password reads as
"wrong". Always SSH **from a real terminal**, not the web console. If you must
reset the password (panel) use **all-lowercase + digits** and **reboot** after
(resets often don't apply until restart).
- To grant a new machine access: append its public key to
`/root/.ssh/authorized_keys` (don't rebuild the server to inject keys).
### Trap #4 — never "Rebuild / reinstall OS"
Rebuild/reinstall **wipes the disk** (DB + reports gone) and can **change the
IP** (breaks API whitelists). Updates go through the deploy steps above —
**only ever `reboot`**, never rebuild. Reboots keep IP + data.
### Trap #5 — egress ≠ inbound IP
APIs that whitelist by IP (Asian Metal: "1 API = 1 IP") see the **egress IP
`31.171.101.234`**, not the SSH IP. Verify any time with:
`ssh -i $KEY root@94.101.187.12 'curl -s https://api.ipify.org'`
## Rollback
`up -d --build` keeps the previous images. If a deploy is bad:
```bash
ssh -i $KEY root@94.101.187.12 \
'cd /root/statista && docker compose -f deploy/docker-compose.yml down && \
docker compose -f deploy/docker-compose.yml up -d && \
docker compose -f deploy/docker-compose.yml restart nginx'
```
For a code rollback, `git archive` an earlier commit/tag in step 1, then rebuild.
## Health check anytime
```bash
ssh -i $KEY root@94.101.187.12 'docker compose -f /root/statista/deploy/docker-compose.yml ps'
ssh -i $KEY root@94.101.187.12 'docker logs deploy-frontend-1 --tail 20'
ssh -i $KEY root@94.101.187.12 'docker logs deploy-backend-1 --tail 20'
```

104
DEPLOY.md Normal file
View File

@ -0,0 +1,104 @@
<div dir="rtl">
# راهنمای دیپلوی روی Liara
این پروژه دو سرویسِ مستقل دارد که جدا دیپلوی می‌شوند:
1. **بک‌اند (API + اسکرپرها)** → پلتفرم **Docker** لیارا (به‌خاطر Playwright/Chromium) + **دیسکِ پایدار** برای SQLite و گزارش‌ها.
2. **فرانت‌اند (TanStack Start)** → پلتفرم **Node** لیارا، یا کلودفلر (تارگتِ پیش‌فرضِ بیلد).
3. **پنل ادمین (`:8001`)** → عمومی نکن؛ فقط لوکال/SSH/VPN.
> قبل از شروع: `npm i -g @liara/cli` و `liara login`.
---
## گام ۱ — بک‌اند روی Liara (Docker)
آرتیفکت‌ها از قبل ساخته شده‌اند: `backend/Dockerfile`، `backend/requirements.txt`، `backend/liara.json`.
```powershell
# ۱) ساخت برنامه (یک‌بار) — پلتفرم docker
liara app:create --name didvan-api --platform docker
# ۲) ساخت دیسکِ پایدار برای دیتابیس + گزارش‌ها (روی /data مونت می‌شود)
liara disk:create --app didvan-api --name data --size 5 # گیگابایت دلخواه
# ۳) تنظیم متغیرهای محیطی (حتماً قبل از دیپلوی)
liara env:set --app didvan-api `
DB_PATH=/data/asianmetal.db `
REPORTS_DIR=/data/reports `
ADMIN_PASSWORD=یک‌رمزِ‌خیلی‌قوی `
ASIANMETAL_TOKEN=توکنِ‌تازهٔ‌منبع `
ALLOWED_ORIGINS=https://didvan.example.com `
COOKIE_SECURE=1 `
COOKIE_SAMESITE=none
# ۴) دیپلوی از داخل پوشهٔ backend
cd backend
liara deploy --app didvan-api --port 8000
```
نکته‌ها:
- `liara.json` داخلِ `backend/` پلتفرم/پورت/دیسک را تعریف کرده؛ پس `liara deploy` را از همان پوشه بزن.
- Chromium در خودِ image نصب می‌شود (`playwright install --with-deps chromium`) → بیلدِ اول کند است. پلنِ حداقل **۱GB RAM**.
- بعد از دیپلوی، آدرسِ بک‌اند مثلاً می‌شود `https://didvan-api.liara.run`.
### آپلودِ آرشیو گزارش‌ها (PDFها)
گزارش‌ها (~۵۰۰۰ فایل) باید روی دیسک باشند. بعد از اولین دیپلوی، از پنل لیارا (بخش دیسک/فایل‌منیجر) یا با ابزارِ لیارا فایل‌ها را در `/data/reports` بریز، با همان ساختار `<منبع> - <دسته>/<file>.pdf`. اگر حجم زیاد است، گزینهٔ بهترْ **آبجکت‌استوریجِ S3ای لیارا** است (نیازمندِ تغییر `reports.py` برای خواندن از S3).
---
## گام ۲ — فرانت‌اند
فرانت با URLِ بک‌اند کار می‌کند؛ متغیرِ بیلد را ست کن:
```
VITE_API_BASE=https://didvan-api.liara.run
```
(در حالت dev این خالی می‌ماند و از پراکسیِ Vite استفاده می‌شود — چیزی خراب نمی‌شود.)
پروژه برای لیارا تنظیم شده تا خروجیِ **سرورِ Node** بدهد (در `vite.config.ts``nitro: { preset: "node-server" }`). بیلد، `.output/server/index.mjs` می‌سازد و اسکریپتِ `start` (در `package.json`) آن را اجرا می‌کند. کانفیگ در `liara.json`ِ ریشه (پلتفرم node، پورت 3000) است.
```powershell
# از ریشهٔ پروژه
liara app:create --name didvan --platform node
liara env:set --app didvan VITE_API_BASE=https://didvan-api.liara.run
liara deploy --app didvan
```
لیارا هنگام دیپلوی `npm install``npm run build``npm start` (یعنی `node .output/server/index.mjs`) را اجرا می‌کند؛ سرورِ Nitro روی متغیرِ `PORT` که لیارا می‌دهد گوش می‌دهد. (تست‌شده: سرور بوت می‌شود و صفحات را سرو می‌کند.)
---
## گام ۳ — کوکیِ ورود (مهم)
ورود با **کوکیِ نشستِ httpOnly** کار می‌کند. بسته به اینکه فرانت و بک‌اند هم‌دامنه‌اند یا نه:
| حالت | تنظیمات بک‌اند |
|---|---|
| **هم‌دامنه** (توصیه‌شده) | `COOKIE_SAMESITE=lax` ، `COOKIE_SECURE=1` |
| **دامنهٔ جدا** (فرانت و API فرق دارند) | `COOKIE_SAMESITE=none` ، `COOKIE_SECURE=1` ، و `ALLOWED_ORIGINS=https://<دامنهٔ فرانت>` |
فرانت همهٔ درخواست‌های احراز را با `credentials:"include"` می‌زند (از قبل انجام شده). CORS هم با `allow_credentials=True` تنظیم شده.
---
## چک‌لیستِ قبل از انتشار
- [ ] توکنِ قدیمیِ منبعِ داده را **باطل** کن (در git history لو رفته) و مقدارِ تازه را در `ASIANMETAL_TOKEN` بگذار.
- [ ] `ADMIN_PASSWORD` قوی (بدونش پنل ادمین بالا نمی‌آید).
- [ ] `COOKIE_SECURE=1` روی HTTPS.
- [ ] `ALLOWED_ORIGINS` دقیقاً دامنهٔ فرانت (نه `*`).
- [ ] `DB_PATH` و `REPORTS_DIR` روی دیسکِ `/data`.
- [ ] پنل ادمین (`:8001`) عمومی نشود.
- [ ] `__pycache__` از گیت پاک شود: `git rm -r --cached backend/__pycache__`.
- [ ] (اختیاری ولی بهتر) مهاجرت SQLite → **PostgreSQLِ مدیریت‌شدهٔ لیارا** برای پایداری/مقیاس.
---
## یادآوریِ معماری
- اسکرپرها داخلِ پروسهٔ بک‌اند (FastAPI lifespan) اجرا می‌شوند؛ تا پروسه بالا باشد کار می‌کنند. ری‌استارت، کشِ حافظه را پاک می‌کند ولی از DB دوباره seed می‌شود.
- جزئیات کامل سیستم در `README.md`.
</div>

299
LIVE-DATA-CATALOG.json Normal file
View File

@ -0,0 +1,299 @@
{
"summary": {
"metals": 0,
"live_commodities": 0,
"tgju_currency": 33,
"tgju_gold": 14,
"tgju_coin": 5,
"yahoo_finance_commodities": 19,
"companies_market_cap_steel_stocks": 81,
"coingecko_crypto": 100,
"coingecko_trending": 15,
"world_economy": 0
},
"generated_at": "2026-07-04T10:52:23Z",
"metals": [],
"live_commodities": [],
"tgju_currency": [
"دلار / نقدی",
"یورو",
"درهم امارات",
"پوند انگلیس",
"لیر ترکیه",
"فرانک سوئیس",
"یوان چین",
"ین ژاپن (100 ین)",
"وون کره جنوبی",
"دلار کانادا",
"دلار استرالیا",
"دلار نیوزیلند",
"دلار سنگاپور",
"روپیه هند",
"روپیه پاکستان",
"دینار عراق",
"لیر سوریه",
"افغانی",
"کرون دانمارک",
"کرون سوئد",
"کرون نروژ",
"ریال عربستان",
"ریال قطر",
"ریال عمان",
"دینار کویت",
"دینار بحرین",
"رینگیت مالزی",
"بات تایلند",
"دلار هنگ کنگ",
"روبل روسیه",
"منات آذربایجان",
"درام ارمنستان",
"لاری گرجستان"
],
"tgju_gold": [
"طلای 18 عیار / 750",
"طلای 18 عیار / 740",
"طلای ۲۴ عیار",
"طلای دست دوم",
"مثقال طلا",
"master.mithqal_world_class",
"مثقال / حواله دلار",
"مثقال / بر مبنای سکه",
"آبشده نقدی",
"آبشده معاملاتی",
"آبشده بنکداری",
"آبشده کمتر از کیلو",
"گرم نقره 925",
"گرم نقره 999"
],
"tgju_coin": [
"سکه امامی",
"سکه بهار آزادی",
"نیم سکه",
"ربع سکه",
"سکه گرمی"
],
"yahoo_finance_commodities": [
"Crude Oil WTI",
"Brent Crude",
"Natural Gas",
"Gasoline",
"Heating Oil",
"Gold",
"Silver",
"Copper",
"Platinum",
"Palladium",
"Aluminum",
"Corn",
"Wheat",
"Soybeans",
"Coffee",
"Cotton",
"Cocoa",
"Sugar",
"Orange Juice"
],
"companies_market_cap_steel_stocks": [
"Nucor",
"ArcelorMittal",
"Steel Dynamics",
"JSW Steel",
"Carpenter Technology",
"Tenaris",
"Tata Steel",
"Reliance",
"China Steel",
"Baosteel",
"Nippon Steel",
"POSCO",
"Jindal Steel & Power",
"Citic Pacific Special Steel Group",
"SSAB",
"BlueScope Steel",
"Steel Authority of India",
"Commercial Metals Company",
"JFE Holdings",
"Severstal",
"Vallourec",
"Aperam",
"Salzgitter",
"Outokumpu",
"Maruichi Steel Tube",
"Alleima AB",
"Usiminas",
"Ternium Argentina",
"Worthington Steel",
"Klöckner & Co",
"Labrador Iron Ore Royalty Corp.",
"Yodogawa Steel Works",
"Ramkrishna Forgings",
"Vesuvius India",
"Mishra Dhatu Nigam",
"Kirloskar Ferrous Industries",
"Metallus",
"Saudi Steel Pipes",
"Insteel Industries",
"Al-Yamamah Steel Industries Company",
"Olympic Steel",
"Kyoei Steel",
"Osaka Steel",
"Chubu Steel Plate",
"Stalprodukt",
"Eisen- und Hüttenwerke",
"Al Jazeera Steel Products",
"Prakash Industries",
"National Metal Manufacturing and Casting",
"ZK International Group",
"Swiss Steel",
"Steel & Tube Holdings",
"Tubos Reunidos",
"Zwahlen & Mayr SA",
"Erdemir",
"Viohalco",
"Kobe Steel",
"Novolipetsk Steel",
"Daido Steel",
"İsdemir",
"Gallantt Ispat",
"Godawari Power & Ispat",
"Aichi Steel",
"Companhia Siderúrgica Nacional",
"Kalyani Steels",
"Algoma Steel",
"ADF Group",
"Arabian Pipes Company",
"MM Forgings",
"Friedman Industries",
"Bisalloy Steel Group",
"Severfield",
"Voestalpine",
"Thyssenkrupp",
"Ternium",
"Gerdau",
"Cleveland-Cliffs",
"Posco International",
"Acerinox",
"Grupo Simec",
"Danieli & C. Officine Meccaniche"
],
"coingecko_crypto": [
"Bitcoin",
"Ethereum",
"Tether",
"BNB",
"USDC",
"XRP",
"Solana",
"TRON",
"Figure Heloc",
"Hyperliquid",
"Dogecoin",
"Rain",
"USDS",
"LEO Token",
"Zcash",
"Stellar",
"WhiteBIT Coin",
"Cardano",
"Monero",
"Chainlink",
"Canton",
"Gram (prev. Toncoin)",
"Dai",
"USD1",
"Bitcoin Cash",
"Ethena USDe",
"Litecoin",
"LAB",
"Hedera",
"Circle USYC",
"Sui",
"Global Dollar",
"Avalanche",
"PayPal USD",
"Cronos",
"Shiba Inu",
"Tether Gold",
"NEAR Protocol",
"BlackRock USD Institutional Digital Liquidity Fund",
"Ondo US Dollar Yield",
"MemeCore",
"Bittensor",
"Uniswap",
"PAX Gold",
"World Liberty Financial",
"Aster",
"OKB",
"Ondo",
"Ripple USD",
"HTX DAO",
"Worldcoin",
"Polkadot",
"Mantle",
"Falcon USD",
"USDD",
"Sky",
"Aave",
"BFUSD",
"Pi Network",
"Morpho",
"Internet Computer",
"Bitget Token",
"Ethereum Classic",
"Pepe",
"DeXe",
"United Stables",
"Spiko EU T-Bills Money Market Fund",
"Quant",
"Blockchain Capital",
"KuCoin",
"Audiera",
"USDGO",
"Janus Henderson Anemoy Treasury Fund",
"Invesco Short Duration US Government Securities Fund",
"Kaspa",
"Stable",
"Render",
"Cosmos Hub",
"Algorand",
"POL (ex-MATIC)",
"Jupiter",
"JUST",
"NEXO",
"USDtb",
"ADI",
"Gate",
"Ethena",
"Beldex",
"币安人生 (BinanceLife)",
"Spiko Amundi Overnight Swap Fund (EUR)",
"Janus Henderson Anemoy AAA CLO Fund",
"Pump.fun",
"Filecoin",
"Flare",
"GHO",
"Venice Token",
"XDC Network",
"Usual USD",
"YLDS",
"Lighter"
],
"coingecko_trending": [
"The Black Bull",
"Pudgy Penguins",
"Hamster Kombat",
"Alien Worlds",
"Solana",
"LAB",
"Solstice",
"Moonbeam",
"Lighter",
"Hyperliquid",
"Pump.fun",
"manlet",
"Siren",
"Bitcoin",
"Gram (prev. Toncoin)"
],
"world_economy": []
}

313
LIVE-DATA-CATALOG.md Normal file
View File

@ -0,0 +1,313 @@
# Live API Data Catalog
Generated at: 2026-07-04T10:52:23Z
## Summary
- metals: 0
- live_commodities: 0
- tgju_currency: 33
- tgju_gold: 14
- tgju_coin: 5
- yahoo_finance_commodities: 19
- companies_market_cap_steel_stocks: 81
- coingecko_crypto: 100
- coingecko_trending: 15
- world_economy: 0
## metals (0)
## live_commodities (0)
## tgju_currency (33)
- دلار / نقدی
- یورو
- درهم امارات
- پوند انگلیس
- لیر ترکیه
- فرانک سوئیس
- یوان چین
- ین ژاپن (100 ین)
- وون کره جنوبی
- دلار کانادا
- دلار استرالیا
- دلار نیوزیلند
- دلار سنگاپور
- روپیه هند
- روپیه پاکستان
- دینار عراق
- لیر سوریه
- افغانی
- کرون دانمارک
- کرون سوئد
- کرون نروژ
- ریال عربستان
- ریال قطر
- ریال عمان
- دینار کویت
- دینار بحرین
- رینگیت مالزی
- بات تایلند
- دلار هنگ کنگ
- روبل روسیه
- منات آذربایجان
- درام ارمنستان
- لاری گرجستان
## tgju_gold (14)
- طلای 18 عیار / 750
- طلای 18 عیار / 740
- طلای ۲۴ عیار
- طلای دست دوم
- مثقال طلا
- master.mithqal_world_class
- مثقال / حواله دلار
- مثقال / بر مبنای سکه
- آبشده نقدی
- آبشده معاملاتی
- آبشده بنکداری
- آبشده کمتر از کیلو
- گرم نقره 925
- گرم نقره 999
## tgju_coin (5)
- سکه امامی
- سکه بهار آزادی
- نیم سکه
- ربع سکه
- سکه گرمی
## yahoo_finance_commodities (19)
- Crude Oil WTI
- Brent Crude
- Natural Gas
- Gasoline
- Heating Oil
- Gold
- Silver
- Copper
- Platinum
- Palladium
- Aluminum
- Corn
- Wheat
- Soybeans
- Coffee
- Cotton
- Cocoa
- Sugar
- Orange Juice
## companies_market_cap_steel_stocks (81)
- Nucor
- ArcelorMittal
- Steel Dynamics
- JSW Steel
- Carpenter Technology
- Tenaris
- Tata Steel
- Reliance
- China Steel
- Baosteel
- Nippon Steel
- POSCO
- Jindal Steel & Power
- Citic Pacific Special Steel Group
- SSAB
- BlueScope Steel
- Steel Authority of India
- Commercial Metals Company
- JFE Holdings
- Severstal
- Vallourec
- Aperam
- Salzgitter
- Outokumpu
- Maruichi Steel Tube
- Alleima AB
- Usiminas
- Ternium Argentina
- Worthington Steel
- Klöckner & Co
- Labrador Iron Ore Royalty Corp.
- Yodogawa Steel Works
- Ramkrishna Forgings
- Vesuvius India
- Mishra Dhatu Nigam
- Kirloskar Ferrous Industries
- Metallus
- Saudi Steel Pipes
- Insteel Industries
- Al-Yamamah Steel Industries Company
- Olympic Steel
- Kyoei Steel
- Osaka Steel
- Chubu Steel Plate
- Stalprodukt
- Eisen- und Hüttenwerke
- Al Jazeera Steel Products
- Prakash Industries
- National Metal Manufacturing and Casting
- ZK International Group
- Swiss Steel
- Steel & Tube Holdings
- Tubos Reunidos
- Zwahlen & Mayr SA
- Erdemir
- Viohalco
- Kobe Steel
- Novolipetsk Steel
- Daido Steel
- İsdemir
- Gallantt Ispat
- Godawari Power & Ispat
- Aichi Steel
- Companhia Siderúrgica Nacional
- Kalyani Steels
- Algoma Steel
- ADF Group
- Arabian Pipes Company
- MM Forgings
- Friedman Industries
- Bisalloy Steel Group
- Severfield
- Voestalpine
- Thyssenkrupp
- Ternium
- Gerdau
- Cleveland-Cliffs
- Posco International
- Acerinox
- Grupo Simec
- Danieli & C. Officine Meccaniche
## coingecko_crypto (100)
- Bitcoin
- Ethereum
- Tether
- BNB
- USDC
- XRP
- Solana
- TRON
- Figure Heloc
- Hyperliquid
- Dogecoin
- Rain
- USDS
- LEO Token
- Zcash
- Stellar
- WhiteBIT Coin
- Cardano
- Monero
- Chainlink
- Canton
- Gram (prev. Toncoin)
- Dai
- USD1
- Bitcoin Cash
- Ethena USDe
- Litecoin
- LAB
- Hedera
- Circle USYC
- Sui
- Global Dollar
- Avalanche
- PayPal USD
- Cronos
- Shiba Inu
- Tether Gold
- NEAR Protocol
- BlackRock USD Institutional Digital Liquidity Fund
- Ondo US Dollar Yield
- MemeCore
- Bittensor
- Uniswap
- PAX Gold
- World Liberty Financial
- Aster
- OKB
- Ondo
- Ripple USD
- HTX DAO
- Worldcoin
- Polkadot
- Mantle
- Falcon USD
- USDD
- Sky
- Aave
- BFUSD
- Pi Network
- Morpho
- Internet Computer
- Bitget Token
- Ethereum Classic
- Pepe
- DeXe
- United Stables
- Spiko EU T-Bills Money Market Fund
- Quant
- Blockchain Capital
- KuCoin
- Audiera
- USDGO
- Janus Henderson Anemoy Treasury Fund
- Invesco Short Duration US Government Securities Fund
- Kaspa
- Stable
- Render
- Cosmos Hub
- Algorand
- POL (ex-MATIC)
- Jupiter
- JUST
- NEXO
- USDtb
- ADI
- Gate
- Ethena
- Beldex
- 币安人生 (BinanceLife)
- Spiko Amundi Overnight Swap Fund (EUR)
- Janus Henderson Anemoy AAA CLO Fund
- Pump.fun
- Filecoin
- Flare
- GHO
- Venice Token
- XDC Network
- Usual USD
- YLDS
- Lighter
## coingecko_trending (15)
- The Black Bull
- Pudgy Penguins
- Hamster Kombat
- Alien Worlds
- Solana
- LAB
- Solstice
- Moonbeam
- Lighter
- Hyperliquid
- Pump.fun
- manlet
- Siren
- Bitcoin
- Gram (prev. Toncoin)
## world_economy (0)

687
PRODUCT-CATALOG.csv Normal file
View File

@ -0,0 +1,687 @@
group,category,title
Base Metals,Copper,Blister Copper TC 98.5%min Delivered China RMB/mt
Base Metals,Copper,Copper Cathode 99.99%min Delivered China RMB/mt
Base Metals,Copper,Copper Conc. 20%min Delivered China RMB/mt Cu
Base Metals,Copper,Copper Conc. TC 25%min CIF China USD/mt
Base Metals,Copper,Copper Rod TC ?8mm EXW China RMB/mt
Base Metals,Copper,Copper Scrap Wire 99%min Delivered China RMB/mt
Base Metals,Lead,Lead Conc. 60%min EXW China RMB/mt Pb
Base Metals,Lead,Lead Conc. TC 50%min CIF China USD/mt
Base Metals,Lead,Lead Ingot 99.994%min In warehouse Guangzhou RMB/mt
Base Metals,Lead,Lead Ingot 99.994%min In warehouse Shanghai RMB/mt
Base Metals,Lead,Lead Ingot Secondary 98%min EXW China RMB/mt
Base Metals,Lead,Lead Ingot Secondary 98%min Ex-VAT EXW China RMB/mt
Base Metals,Lead,Lead Ingot Secondary 99.994%min EXW China RMB/mt
Base Metals,Lead,Lead Ingot Secondary 99.994%min Ex-VAT EXW China RMB/mt
Base Metals,Lead,Lead Scrap Recycled Deep Cycle Battery Ex-VAT Delivered China RMB/mt
Base Metals,Lead,Lead Scrap Recycled Start-type Battery Ex-VAT Delivered China RMB/mt
Base Metals,Nickel,Nickel Cathode 99.96%min EXW China RMB/mt
Base Metals,Nickel,Nickel Cathode Norilsk 99.96%min In port China RMB/mt
Base Metals,Nickel,Nickel Ore Philippine 0.8%min CIF China USD/mt
Base Metals,Nickel,Nickel Ore Philippine 0.8%min In port China RMB/mt
Base Metals,Nickel,Nickel Ore Philippine 1.3%min CIF China USD/mt
Base Metals,Nickel,Nickel Ore Philippine 1.5%min CIF China USD/mt
Base Metals,Nickel,Nickel Ore Philippine 1.5%min In port China RMB/mt
Base Metals,Nickel,Nickel Ore Philippine 1.8%min CIF China USD/mt
Base Metals,Nickel,Nickel Sulfate Ni 22%min; Co 0.05%max Delivered China RMB/mt
Base Metals,Nickel,Nickel Sulfate Ni 22%min; Co 0.4%max Delivered China RMB/mt
Base Metals,Tin,Tin Conc. 60%min Delivered China RMB/mtm
Base Metals,Tin,Tin Conc. Burmese 20%min In warehouse MengA RMB/mtm
Base Metals,Tin,Tin Conc. Burmese 30%min In warehouse MengA RMB/mtm
Base Metals,Tin,Tin Ingot 99.9%min EXW China RMB/mt
Base Metals,Tin,"Tin Solder Wire Sn 99%min, Ag 0.3%min, Cu 0.7%max EXW China RMB/kg"
Base Metals,Tin,"Tin Solder Wire Sn 99%min, Cu 0.7%max EXW China RMB/kg"
Base Metals,Zinc,Zinc Conc. 50%min EXW North China RMB/mt Zn
Base Metals,Zinc,Zinc Conc. TC 50% CIF China USD/mt
Carbon Steel,Coated,HGI Coil DX51D 0.5mm FOB China USD/mt
Carbon Steel,Coated,HGI Coil DX51D 0.5mm In warehouse Guangzhou RMB/mt
Carbon Steel,Coated,HGI Coil DX51D 0.5mm In warehouse Shanghai RMB/mt
Carbon Steel,Coated,HGI Coil DX51D 0.5mm In warehouse Tianjin RMB/mt
Carbon Steel,Coated,HGI Coil DX51D 1.0mm FOB China USD/mt
Carbon Steel,Coated,HGI Coil DX51D 1.0mm In warehouse Guangzhou RMB/mt
Carbon Steel,Coated,HGI Coil DX51D 1.0mm In warehouse Shanghai RMB/mt
Carbon Steel,Coated,HGI Coil DX51D 1.0mm In warehouse Tianjin RMB/mt
Carbon Steel,Coated,HGI Coil IS513 0.5mm In warehouse Mumbai INR/mt
Carbon Steel,Cold Rolled Coil,CR Coil DC01 1.0mm In warehouse Istanbul USD/mt
Carbon Steel,Cold Rolled Coil,CR Coil IS513 1.0mm In warehouse Mumbai INR/mt
Carbon Steel,Cold Rolled Coil,CR Coil SPCC 0.5mm FOB China USD/mt
Carbon Steel,Cold Rolled Coil,CR Coil SPCC 0.5mm In warehouse Guangzhou RMB/mt
Carbon Steel,Cold Rolled Coil,CR Coil SPCC 0.5mm In warehouse Shanghai RMB/mt
Carbon Steel,Cold Rolled Coil,CR Coil SPCC 0.5mm In warehouse Tianjin RMB/mt
Carbon Steel,Cold Rolled Coil,CR Coil SPCC 1.0mm FOB China USD/mt
Carbon Steel,Cold Rolled Coil,CR Coil SPCC 1.0mm In warehouse Guangzhou RMB/mt
Carbon Steel,Cold Rolled Coil,CR Coil SPCC 1.0mm In warehouse Hanoi VND/mt
Carbon Steel,Cold Rolled Coil,CR Coil SPCC 1.0mm In warehouse Kuala Lumpur MYR/mt
Carbon Steel,Cold Rolled Coil,CR Coil SPCC 1.0mm In warehouse Shanghai RMB/mt
Carbon Steel,Cold Rolled Coil,CR Coil SPCC 1.0mm In warehouse Tianjin RMB/mt
Carbon Steel,Cold Rolled Coil,"LaCe Chloride TREO 45%min, CeO2/REO 65±2% EXW China RMB/mt"
Carbon Steel,Cold Rolled Coil,"LaCe Mischmetal Ce/RE 65%min, Fe 0.3%max EXW China RMB/mt"
Carbon Steel,Cold Rolled Coil,Lanthanum Oxide 99.9%min FOB China USD/mt
Carbon Steel,Cold Rolled Coil,Lanthanum Oxide 99.999%min EXW China RMB/mt
Carbon Steel,Cold Rolled Coil,Lanthanum Oxide 99.999%min FOB China USD/kg
Carbon Steel,Hot Rolled Coil,Ferro-gadolinium Gd 73% EXW China RMB/mt
Carbon Steel,Hot Rolled Coil,Ferro-holmium 80% EXW China RMB/kg
Carbon Steel,Hot Rolled Coil,Gadolinium Oxide 99.5%min EXW China RMB/mt
Carbon Steel,Hot Rolled Coil,Gadolinium Oxide 99.99%min EXW China RMB/mt
Carbon Steel,Hot Rolled Coil,HR Coil S275 JR 3.0mm In warehouse Istanbul USD/mt
Carbon Steel,Hot Rolled Coil,HR Coil IS2062 3.0mm In warehouse Mumbai INR/mt
Carbon Steel,Hot Rolled Coil,HR Coil Q235B 2.75mm In warehouse Guangzhou RMB/mt
Carbon Steel,Hot Rolled Coil,HR Coil Q235B 2.75mm In warehouse Shanghai RMB/mt
Carbon Steel,Hot Rolled Coil,HR Coil Q235B 2.75mm In warehouse Tianjin RMB/mt
Carbon Steel,Hot Rolled Coil,HR Coil Q235B 5.5mm In warehouse Guangzhou RMB/mt
Carbon Steel,Hot Rolled Coil,HR Coil Q235B 5.5mm In warehouse Shanghai RMB/mt
Carbon Steel,Hot Rolled Coil,HR Coil Q235B 5.5mm In warehouse Tianjin RMB/mt
Carbon Steel,Hot Rolled Coil,HR Coil SS400 2.75mm FOB China USD/mt
Carbon Steel,Hot Rolled Coil,HR Coil SS400 3.0mm In warehouse Bangkok THB/mt
Carbon Steel,Hot Rolled Coil,HR Coil SS400 3.0mm In warehouse Hanoi VND/mt
Carbon Steel,Hot Rolled Coil,HR Coil SS400 3.0mm In warehouse Kuala Lumpur MYR/mt
Carbon Steel,Hot Rolled Coil,HR Coil SS400 5.5mm FOB China USD/mt
Carbon Steel,Hot Rolled Coil,Holmium Oxide 99.5%min EXW China RMB/kg
Carbon Steel,Hot Rolled Coil,Lanthanum Chloride 99.9%min EXW China RMB/mt
Carbon Steel,Hot Rolled Coil,Lanthanum Metal 99%min EXW China RMB/mt
Carbon Steel,Hot Rolled Coil,Lanthanum Metal 99%min FOB China USD/kg
Carbon Steel,Hot Rolled Coil,Lanthanum Oxide 99.9%min EXW China RMB/mt
Carbon Steel,Pipe,Seamless Pipe 20# ?219*6mm GB8163 In warehouse Guangzhou RMB/mt
Carbon Steel,Pipe,Seamless Pipe 20# ?219*6mm GB8163 In warehouse Shanghai RMB/mt
Carbon Steel,Pipe,Seamless Pipe 20# ?219*6mm GB8163 In warehouse Tianjin RMB/mt
Carbon Steel,Pipe,Welded Pipe Q235 ?114*3.75mm In warehouse Guangzhou RMB/mt
Carbon Steel,Pipe,Welded Pipe Q235 ?114*3.75mm In warehouse Shanghai RMB/mt
Carbon Steel,Pipe,Welded Pipe Q235 ?114*3.75mm In warehouse Tianjin RMB/mt
Carbon Steel,Plate,Medium Plate IS2062 16mm In warehouse Mumbai INR/mt
Carbon Steel,Plate,Medium Plate Q235B 10mm In warehouse Guangzhou RMB/mt
Carbon Steel,Plate,Medium Plate Q235B 10mm In warehouse Shanghai RMB/mt
Carbon Steel,Plate,Medium Plate Q235B 10mm In warehouse Tianjin RMB/mt
Carbon Steel,Plate,Medium Plate Q235B 16mm In warehouse Guangzhou RMB/mt
Carbon Steel,Plate,Medium Plate Q235B 16mm In warehouse Shanghai RMB/mt
Carbon Steel,Plate,Medium Plate Q235B 16mm In warehouse Tianjin RMB/mt
Carbon Steel,Plate,Medium Plate S275 JR 16mm In warehouse Istanbul USD/mt
Carbon Steel,Plate,Medium Plate SS400 10mm FOB China USD/mt
Carbon Steel,Plate,Medium Plate SS400 16mm FOB China USD/mt
Carbon Steel,Plate,Medium Plate SS400 16mm In warehouse Bangkok THB/mt
Carbon Steel,Plate,Medium Plate SS400 16mm In warehouse Hanoi VND/mt
Carbon Steel,Plate,Medium Plate SS400 16mm In warehouse Kuala Lumpur MYR/mt
Carbon Steel,Rebar,Rebar BS4449 GR500 12mm In warehouse Kuala Lumpur MYR/mt
Carbon Steel,Rebar,Rebar BS4449 GR500 16mm In warehouse Kuala Lumpur MYR/mt
Carbon Steel,Rebar,Rebar CB400V/SD390 ?14-32mm In warehouse Hanoi VND/mt
Carbon Steel,Rebar,Rebar HRB400 12mm FOB China USD/mt
Carbon Steel,Rebar,Rebar HRB400 20mm FOB China USD/mt
Carbon Steel,Rebar,Rebar HRB400 20mm In warehouse Guangzhou RMB/mt
Carbon Steel,Rebar,Rebar HRB400 20mm In warehouse Shanghai RMB/mt
Carbon Steel,Rebar,Rebar HRB400 20mm In warehouse Tianjin RMB/mt
Carbon Steel,Rebar,TMT Bar Fe500 12mm In warehouse Mumbai INR/mt
Carbon Steel,Rebar,TMT Bar Fe500 16mm In warehouse Mumbai INR/mt
Carbon Steel,Sections,Angle Steel Q235 50*50*5mm In warehouse Guangzhou RMB/mt
Carbon Steel,Sections,Angle Steel Q235 50*50*5mm In warehouse Shanghai RMB/mt
Carbon Steel,Sections,Angle Steel Q235 50*50*5mm In warehouse Tianjin RMB/mt
Carbon Steel,Sections,Channel Steel Q235 160*65*8.5mm In warehouse Guangzhou RMB/mt
Carbon Steel,Sections,Channel Steel Q235 160*65*8.5mm In warehouse Shanghai RMB/mt
Carbon Steel,Sections,Channel Steel Q235 160*65*8.5mm In warehouse Tianjin RMB/mt
Carbon Steel,Sections,Dysprosium Metal 99.5%min EXW China RMB/kg
Carbon Steel,Sections,Dysprosium Metal 99.5%min FOB China USD/kg
Carbon Steel,Sections,Dysprosium Oxide 99.5%min EXW China RMB/kg
Carbon Steel,Sections,Dysprosium Oxide 99.5%min FOB China USD/kg
Carbon Steel,Sections,Erbium Oxide 99.5%min EXW China RMB/kg
Carbon Steel,Sections,Erbium Oxide 99.5%min FOB China USD/kg
Carbon Steel,Sections,Europium Metal 99.5%min FOB China USD/kg
Carbon Steel,Sections,Europium Oxide 99.999%min EXW China RMB/kg
Carbon Steel,Sections,Europium Oxide 99.999%min FOB China USD/kg
Carbon Steel,Sections,Ferro-dysprosium 80% EXW China RMB/kg
Carbon Steel,Sections,Ferro-dysprosium 80% FOB China USD/kg
Carbon Steel,Sections,H-beam Steel Q235 200*200mm In warehouse Guangzhou RMB/mt
Carbon Steel,Sections,H-beam Steel Q235 200*200mm In warehouse Shanghai RMB/mt
Carbon Steel,Sections,H-beam Steel Q235 200*200mm In warehouse Tianjin RMB/mt
Carbon Steel,Sections,H-beam Steel Q235 300*300mm In warehouse Guangzhou RMB/mt
Carbon Steel,Sections,H-beam Steel Q235 300*300mm In warehouse Shanghai RMB/mt
Carbon Steel,Sections,H-beam Steel Q235 300*300mm In warehouse Tianjin RMB/mt
Carbon Steel,Sections,I-beam Steel Q235 250*118*10mm In warehouse Guangzhou RMB/mt
Carbon Steel,Sections,I-beam Steel Q235 250*118*10mm In warehouse Shanghai RMB/mt
Carbon Steel,Sections,I-beam Steel Q235 250*118*10mm In warehouse Tianjin RMB/mt
Carbon Steel,Strip,HR Strip Q195 295*2.5mm In warehouse Guangzhou RMB/mt
Carbon Steel,Strip,HR Strip Q195 295*2.5mm In warehouse Shanghai RMB/mt
Carbon Steel,Strip,HR Strip Q195 295*2.5mm In warehouse Tianjin RMB/mt
Carbon Steel,Strip,HR Strip Q195 685*2.75mm In warehouse Guangzhou RMB/mt
Carbon Steel,Strip,HR Strip Q235B 685*5.5mm In warehouse Shanghai RMB/mt
Carbon Steel,Strip,HR Strip Q235B 685*5.5mm In warehouse Tianjin RMB/mt
Carbon Steel,Wire Rod,"LaCe Mischmetal Ce/RE 65%min, Fe 0.3%max EXW China RMB/mt"
Carbon Steel,Wire Rod,"LaCe Mischmetal Ce/RE 65%min, Fe 1%max EXW China RMB/mt"
Carbon Steel,Wire Rod,"LaCe Mischmetal Ce/RE 65%min, Fe 1%max FOB China USD/kg"
Carbon Steel,Wire Rod,Wire Rod JIS SWRM15 ?6.5mm In warehouse Bangkok THB/mt
Carbon Steel,Wire Rod,Wire Rod Q195 6.5mm FOB China USD/mt
Carbon Steel,Wire Rod,Wire Rod Q235 8mm In warehouse Guangzhou RMB/mt
Carbon Steel,Wire Rod,Wire Rod Q235 8mm In warehouse Shanghai RMB/mt
Carbon Steel,Wire Rod,Wire Rod Q235 8mm In warehouse Tianjin RMB/mt
Carbon Steel,Wire Rod,Wire Rod SAE1008 5.5mm In warehouse Kuala Lumpur MYR/mt
Carbon Steel,Wire Rod,Wire Rod SAE1008 5.5mm In warehouse Mumbai INR/mt
Carbon Steel,Wire Rod,Wire Rod SAE1008 6.5mm FOB China USD/mt
Ferroalloys,Calcium Silicon,Calcium-silicon 28-55 EXW China RMB/mt
Ferroalloys,Calcium Silicon,Calcium-silicon 28-55 FOB China USD/mt
Ferroalloys,Calcium Silicon,Calcium-silicon 30-60 EXW China RMB/mt
Ferroalloys,Calcium Silicon,Calcium-silicon 30-60 FOB China USD/mt
Ferroalloys,Chrome Ore,Chrome Conc. 40%min FOB S.A. USD/mt
Ferroalloys,Chrome Ore,Chrome Conc. 42%min FOB S.A. USD/mt
Ferroalloys,Chrome Ore,Chrome Conc. 46%min FOB Turkey USD/mt
Ferroalloys,Chrome Ore,Chrome Conc. S.A. 40%min CIF China USD/mt
Ferroalloys,Chrome Ore,Chrome Conc. S.A. 40%min In port China RMB/dmtu
Ferroalloys,Chrome Ore,Chrome Conc. S.A. 42%min CIF China USD/mt
Ferroalloys,Chrome Ore,Chrome Conc. S.A. 42%min In port China RMB/dmtu
Ferroalloys,Chrome Ore,Chrome Conc. Turkish 46%min CIF China USD/mt
Ferroalloys,Chrome Ore,Chrome Conc. Turkish 46%min In port China RMB/dmtu
Ferroalloys,Chrome Ore,Chrome Conc. Zimbabwean 48%min CIF China USD/mt
Ferroalloys,Chrome Ore,Chrome Conc. Zimbabwean 48%min In port China RMB/dmtu
Ferroalloys,Chrome Ore,Chrome Lump 40%min FOB Turkey USD/mt
Ferroalloys,Chrome Ore,Chrome Lump S.A. 38%min CIF China USD/mt
Ferroalloys,Chrome Ore,Chrome Lump S.A. 38%min In port China RMB/dmtu
Ferroalloys,Chrome Ore,Chrome Lump Turkish 40%min CIF China USD/mt
Ferroalloys,Chrome Ore,Chrome Lump Turkish 40%min In port China RMB/dmtu
Ferroalloys,Chromium Silicon,"Silico-chrome Si 42%min, Cr 30%min EXW China RMB/mt"
Ferroalloys,Ferroboron,Ferro-boron B 18%min EXW China RMB/mt
Ferroalloys,Ferroboron,Ferro-boron B 18%min FOB China USD/mt
Ferroalloys,Ferroboron,Ferro-boron B 19%min EXW China RMB/mt
Ferroalloys,Ferrochrome,"Ferro-chrome Cr 52%min, C 8%max EXW China RMB/mt 50-basis"
Ferroalloys,Ferrochrome,"Ferro-chrome Cr 55%min, C 0.1%max EXW China RMB/mt 60-basis"
Ferroalloys,Ferrochrome,"Ferro-chrome Cr 55%min, C 0.25%max EXW China RMB/mt 60-basis"
Ferroalloys,Ferrochrome,"Ferro-chrome Cr 60%min, C 0.1%max FOB China USD/lb Cr"
Ferroalloys,Ferrochrome,"Ferro-chrome Cr 60%min, C 0.1%max In warehouse Pittsburgh USD/lb Cr"
Ferroalloys,Ferrochrome,"Ferro-chrome Cr 60%min, C 0.1%max In warehouse Rotterdam USD/lb Cr"
Ferroalloys,Ferrochrome,"Ferro-chrome Cr 60%min, C 8%max In warehouse Pittsburg USD/lb Cr"
Ferroalloys,Ferrochrome,"Ferro-chrome Cr 60%min, C 8%max In warehouse Rotterdam USD/lb Cr"
Ferroalloys,Ferrochrome,"Ferro-chrome Cr 60%min, C 8.5%max In warehouse Russia RUB/kg"
Ferroalloys,Ferrochrome,"Ferro-chrome Cr 65%min, C 0.1%max In warehouse Russia RUB/kg"
Ferroalloys,Ferrochrome,"Ferro-chrome Indian Cr 58%min, C 8%max CIF China USD/lb Cr"
Ferroalloys,Ferrochrome,"Ferro-chrome Indian Cr 58%min, C 8%max In port China RMB/mt 50-basis"
Ferroalloys,Ferrochrome,"Ferro-chrome Kazakhstan Cr 68%min, C 8.5%max CIF China USD/lb Cr"
Ferroalloys,Ferrochrome,"Ferro-chrome Kazakhstan Cr 68%min, C 8.5%max In port China RMB/mt 50-basis"
Ferroalloys,Ferrochrome,"Ferro-chrome S.A. Cr 50%min, C 8%max CIF China USD/lb Cr"
Ferroalloys,Ferrochrome,"Ferro-chrome S.A. Cr 50%min, C 8%max In port China RMB/mt 50-basis"
Ferroalloys,Ferromanganese,"Ferro-manganese Mn 65%min, C 7%max EXW China RMB/mt"
Ferroalloys,Ferromanganese,"Ferro-manganese Mn 75%min, C 2%max EXW China RMB/mt"
Ferroalloys,Ferromanganese,"Ferro-manganese Mn 75%min, C 8%max EXW China RMB/mt"
Ferroalloys,Ferromanganese,"Ferro-manganese Mn 78%min, C 1.5%max EXW China RMB/mt"
Ferroalloys,Ferromanganese,"Ferro-manganese Mn 78%min, C 7%max In warehouse Russia RUB/kg"
Ferroalloys,Ferromanganese,"Ferro-manganese Mn 78%min, C 8%max Delivered Europe EUR/mt"
Ferroalloys,Ferromanganese,"Ferro-manganese Mn 78%min, C 8%max In warehouse Pittsburgh USD/gt"
Ferroalloys,Ferromanganese,"Ferro-manganese Mn 80%min, C 0.7%max EXW China RMB/mt"
Ferroalloys,Ferromolybdenum,Ferro-molybdenum 60%min Delivered India INR/kg
Ferroalloys,Ferromolybdenum,Ferro-molybdenum 60%min EXW China RMB/mt
Ferroalloys,Ferromolybdenum,Ferro-molybdenum 60%min In port South Korea USD/kg Mo
Ferroalloys,Ferromolybdenum,Ferro-molybdenum 60%min In warehouse Russia RUB/kg
Ferroalloys,Ferromolybdenum,Ferro-molybdenum 65%min In warehouse Rotterdam USD/kg Mo
Ferroalloys,Ferromolybdenum,Ferro-molybdenum 65%min In warehouse US USD/kg Mo
Ferroalloys,Ferromolybdenum,Molybdenum Oxide 57%min In port South Korea USD/lb
Ferroalloys,Ferromolybdenum,Molybdenum Oxide 57%min In warehouse Rotterdam USD/lb
Ferroalloys,Ferronickel,Ferro-nickel 1.5%min EXW China RMB/mt
Ferroalloys,Ferronickel,Ferro-nickel 10%min EXW China RMB/mtu
Ferroalloys,Ferronickel,Nickel Ore Philippine 1.5%min CIF China USD/mt
Ferroalloys,Ferronickel,Nickel Ore Philippine 1.8%min In port China RMB/mt
Ferroalloys,Ferroniobium,Ferro-niobium 50-A EXW China RMB/mt
Ferroalloys,Ferroniobium,Ferro-niobium 60-A EXW China RMB/mt
Ferroalloys,Ferroniobium,Ferro-niobium Brazilian 66% Delivered China RMB/mt
Ferroalloys,Ferroniobium,Ferro-niobium Brazilian 66% In warehouse Rotterdam USD/kg Nb
Ferroalloys,Ferroniobium,"Niobium Conc. Nb2O5 50%min, Ta2O5 5%min, CIF China USD/lb Oxides"
Ferroalloys,Ferroniobium,Niobium Pentoxide 99.5%min EXW China RMB/kg
Ferroalloys,Ferrophosphorus,Ferro-phosphorus 23%min FOB China USD/mt
Ferroalloys,Ferrosilicon,Ferro-silicon 65%min FOB Russia USD/mt
Ferroalloys,Ferrosilicon,Ferro-silicon 65%min In warehouse Russia RUB/kg
Ferroalloys,Ferrosilicon,Ferro-silicon 70%min EXW India INR/mt
Ferroalloys,Ferrosilicon,Ferro-silicon 72%min EXW China RMB/mt
Ferroalloys,Ferrosilicon,Ferro-silicon 72%min Ex-VAT FOB China USD/mt
Ferroalloys,Ferrosilicon,Ferro-silicon 72%min FOB China USD/mt
Ferroalloys,Ferrosilicon,Ferro-silicon 75%min EXW China RMB/mt
Ferroalloys,Ferrosilicon,Ferro-silicon 75%min Ex-VAT FOB China USD/mt
Ferroalloys,Ferrosilicon,Ferro-silicon 75%min FOB China USD/mt
Ferroalloys,Ferrosilicon,Ferro-silicon 75%min In warehouse Pittsburgh USD/lb Si
Ferroalloys,Ferrosilicon,Ferro-silicon 75%min In warehouse Rotterdam EUR/mt
Ferroalloys,Ferrotitanium,Ferro-titanium 30% EXW China RMB/mt 25-basis
Ferroalloys,Ferrotitanium,Ferro-titanium 30% EXW India INR/kg
Ferroalloys,Ferrotitanium,Ferro-titanium 70% (scrap) EXW China RMB/mt
Ferroalloys,Ferrotitanium,Ferro-titanium 70% In port India INR/kg
Ferroalloys,Ferrotitanium,Ferro-titanium 70%min Delivered US USD/kg Ti
Ferroalloys,Ferrotitanium,Ferro-titanium 70%min EXW Russia RUB/kg
Ferroalloys,Ferrotitanium,Ferro-titanium 70%min FOB Russia USD/kg Ti
Ferroalloys,Ferrotitanium,Ferro-titanium 70%min In warehouse Rotterdam USD/kg Ti
Ferroalloys,Ferrotungsten,Ferro-tungsten 70%min EXW China RMB/mt
Ferroalloys,Ferrotungsten,Ferro-tungsten 75%min EXW China RMB/mt
Ferroalloys,Ferrotungsten,Ferro-tungsten 75%min FOB China USD/kg W
Ferroalloys,Ferrotungsten,Ferro-tungsten 75%min FOB Russia USD/kg W
Ferroalloys,Ferrotungsten,Ferro-tungsten 75%min In warehouse Rotterdam USD/kg W
Ferroalloys,Ferrovanadium,Ferro-vanadium 50%min CIF China USD/kg V
Ferroalloys,Ferrovanadium,Ferro-vanadium 50%min Delivered China RMB/mt
Ferroalloys,Ferrovanadium,Ferro-vanadium 50%min In port China RMB/mt
Ferroalloys,Ferrovanadium,Ferro-vanadium 50%min In warehouse Russia RUB/kg
Ferroalloys,Ferrovanadium,Ferro-vanadium 80%min FOB China USD/kg V
Ferroalloys,Ferrovanadium,Ferro-vanadium 80%min In warehouse Pittsburgh USD/kg V
Ferroalloys,Ferrovanadium,Ferro-vanadium 80%min In warehouse Rotterdam USD/kg V
Ferroalloys,Ferrovanadium,Vanadium Nitride N 14%min EXW China RMB/mt
Ferroalloys,Ferrovanadium,Vanadium Pentoxide Flake 98%min In warehouse Rotterdam USD/lb V2O5
Ferroalloys,Manganese Ore,Manganese Ore 38%min FOB S.A. USD/dmtu
Ferroalloys,Manganese Ore,Manganese Ore Australian 45%min In Qinzhou Port RMB/mtu
Ferroalloys,Manganese Ore,Manganese Ore Australian 45%min In Tianjin Port RMB/mtu
Ferroalloys,Manganese Ore,Manganese Ore Brazilian 44%min In Qinzhou Port RMB/mtu
Ferroalloys,Manganese Ore,Manganese Ore Brazilian 44%min In Tianjin Port RMB/mtu
Ferroalloys,Manganese Ore,"Manganese Ore Carbonate, Mn 13%min EXW China RMB/mtu"
Ferroalloys,Manganese Ore,Manganese Ore Gabonese 44%min In Qinzhou Port RMB/mtu
Ferroalloys,Manganese Ore,Manganese Ore Gabonese 44%min In Tianjin Port RMB/mtu
Ferroalloys,Manganese Ore,Manganese Ore S.A. 36%min In Qinzhou Port RMB/mtu
Ferroalloys,Manganese Ore,Manganese Ore S.A. 36%min In Tianjin Port RMB/mtu
Ferroalloys,Manganese Ore,Manganese Ore S.A. 37%min In Qinzhou Port RMB/mtu
Ferroalloys,Manganese Ore,Manganese Ore S.A. 37%min In Tianjin Port RMB/mtu
Ferroalloys,Silicomanganese,Silico-manganese 60/14 EXW China RMB/mt
Ferroalloys,Silicomanganese,Silico-manganese 60/14 FOB India USD/mt
Ferroalloys,Silicomanganese,Silico-manganese 65/17 Delivered Europe EUR/mt
Ferroalloys,Silicomanganese,Silico-manganese 65/17 EXW China RMB/mt
Ferroalloys,Silicomanganese,Silico-manganese 65/17 FOB India USD/mt
Ferroalloys,Silicomanganese,Silico-manganese 65/17 In warehouse Pittsburgh USD/lb
Ferroalloys,Silicomanganese,Silico-manganese 65/17 In warehouse Russia RUB/kg
Minor Metals,Antimony,Antimony Conc. 50%min Delivered China RMB/mt Sb
Minor Metals,Antimony,Antimony Ingot 99.65%min EXW China RMB/mt
Minor Metals,Antimony,Antimony Ingot 99.65%min In port India INR/kg
Minor Metals,Antimony,Antimony Ingot 99.65%min In warehouse Baltimore USD/lb
Minor Metals,Antimony,Antimony Ingot 99.65%min In warehouse Rotterdam USD/mt
Minor Metals,Antimony,Antimony Ingot 99.85%min EXW China RMB/mt
Minor Metals,Antimony,Antimony Trioxide 99.5%min Delivered China RMB/mt
Minor Metals,Antimony,Antimony Trioxide 99.5%min FOB China USD/mt
Minor Metals,Antimony,Antimony Trioxide 99.5%min In warehouse Baltimore USD/lb
Minor Metals,Antimony,Antimony Trioxide 99.5%min In warehouse Rotterdam EUR/kg
Minor Metals,Antimony,Antimony Trioxide 99.8%min Delivered China RMB/mt
Minor Metals,Antimony,Ethylene Glycol Antimony 57%min Delivered China RMB/mt
Minor Metals,Arsenic,Arsenic Metal 99%min EXW China RMB/mt
Minor Metals,Arsenic,Arsenic Metal 99%min FOB China USD/mt
Minor Metals,Arsenic,Arsenic Trioxide 99%min EXW China RMB/mt
Minor Metals,Arsenic,Arsenic Trioxide 99%min FOB China USD/mt
Minor Metals,Bismuth,Bismuth Ingot 99.99%min Delivered Europe USD/lb
Minor Metals,Bismuth,Bismuth Ingot 99.99%min Delivered India INR/kg
Minor Metals,Bismuth,Bismuth Ingot 99.99%min Delivered US USD/lb
Minor Metals,Bismuth,Bismuth Ingot 99.99%min EXW China RMB/mt
Minor Metals,Bismuth,Bismuth Ingot 99.99%min FOB China USD/lb
Minor Metals,Bismuth,Bismuth Oxide 99.9%min EXW China RMB/mt
Minor Metals,Cadmium,Cadmium Ingot 99.99%min Delivered India INR/kg
Minor Metals,Cadmium,Cadmium Ingot 99.99%min EXW China RMB/mt
Minor Metals,Cadmium,Cadmium Ingot 99.99%min In warehouse Baltimore USD/lb
Minor Metals,Cadmium,Cadmium Ingot 99.99%min In warehouse Rotterdam USD/lb
Minor Metals,Calcium,Calcium Granule Electrolytic 98.5%min EXW China RMB/mt
Minor Metals,Calcium,Calcium Granule Electrolytic 98.5%min FOB China USD/mt
Minor Metals,Calcium,Calcium Granule Reductive 98%min EXW China RMB/mt
Minor Metals,Calcium,Calcium Granule Reductive 98%min FOB China USD/mt
Minor Metals,Calcium,Calcium Ingot Reductive 98%min EXW China RMB/mt
Minor Metals,Calcium,Fluorspar 97%min Delivered China RMB/mt
Minor Metals,Chromium,Chromic Anhydride 99.8%min EXW China RMB/mt
Minor Metals,Chromium,Chromium Metal 99%min EXW China RMB/mt
Minor Metals,Chromium,Chromium Metal 99%min FOB China USD/mt
Minor Metals,Chromium,Chromium Oxide Green 99%min EXW China RMB/mt
Minor Metals,Cobalt,Cobalt Carbonate 46%min Delivered China RMB/mt
Minor Metals,Cobalt,Cobalt Chloride 24%min Delivered China RMB/mt
Minor Metals,Cobalt,Cobalt Conc. 6-8% CIF China USD/lb Co
Minor Metals,Cobalt,Cobalt Intermediate 20-30% CIF China USD/lb Co
Minor Metals,Cobalt,Cobalt Metal 99.8%min Delivered China RMB/kg
Minor Metals,Cobalt,Cobalt Metal 99.8%min In warehouse Baltimore USD/lb
Minor Metals,Cobalt,Cobalt Metal 99.8%min In warehouse Rotterdam USD/lb
Minor Metals,Cobalt,Cobalt Monoxide 78%min Delivered China RMB/kg
Minor Metals,Cobalt,Cobalt Oxide 72%min Delivered China RMB/kg
Minor Metals,Cobalt,Cobalt Oxide 72%min In warehouse Rotterdam EUR/kg
Minor Metals,Cobalt,Cobalt Powder 99.5%min Delivered China RMB/kg
Minor Metals,Cobalt,Cobalt Sulfate 20.5%min Delivered China RMB/mt
Minor Metals,Cobalt,Cobalt Tetroxide 73.5%min Delivered China RMB/kg
Minor Metals,Cobalt,Lithium Cobaltate Co 60%min Delivered China RMB/kg
Minor Metals,Cobalt,NCM 523 Delivered China RMB/mt
Minor Metals,Cobalt,NCM 622 Delivered China RMB/mt
Minor Metals,Cobalt,Samarium Cobalt 30H EXW China RMB/kg
Minor Metals,Gallium,Gallium Metal 99.99%min EXW China RMB/kg
Minor Metals,Gallium,Gallium Metal 99.99%min FOB China USD/kg
Minor Metals,Gallium,Gallium Metal 99.99999%min EXW China RMB/kg
Minor Metals,Germanium,Germanium Dioxide 99.99%min Delivered US USD/kg
Minor Metals,Germanium,Germanium Dioxide 99.99%min EXW China RMB/kg
Minor Metals,Germanium,Germanium Dioxide 99.99%min FOB China USD/kg
Minor Metals,Germanium,Germanium Metal 99.99%min Delivered Europe USD/kg
Minor Metals,Germanium,Germanium Metal 99.99%min Delivered US USD/kg
Minor Metals,Germanium,Germanium Metal 99.99%min EXW China RMB/kg
Minor Metals,Germanium,Germanium Metal 99.99%min FOB China USD/kg
Minor Metals,Indium,Indium Ingot 99.995%min Delivered Europe USD/kg
Minor Metals,Indium,Indium Ingot 99.995%min Delivered US USD/kg
Minor Metals,Indium,Indium Ingot 99.995%min EXW China RMB/kg
Minor Metals,Indium,Indium Ingot 99.995%min FOB China USD/kg
Minor Metals,Lithium,LNCMO 523 Delivered China RMB/kg
Minor Metals,Lithium,LNCMO 622 Delivered China RMB/kg
Minor Metals,Lithium,Lithium Carbonate 99%min Delivered China RMB/mt
Minor Metals,Lithium,Lithium Carbonate 99.5%min CIF China USD/kg
Minor Metals,Lithium,Lithium Carbonate 99.5%min Delivered China RMB/mt
Minor Metals,Lithium,Lithium Carbonate 99.5%min Delivered EU USD/kg
Minor Metals,Lithium,Lithium Carbonate 99.5%min Delivered US USD/kg
Minor Metals,Lithium,Lithium Carbonate 99.5%min FOB South America USD/kg
Minor Metals,Lithium,Lithium Chloride 99.3%min Delivered China RMB/mt
Minor Metals,Lithium,Lithium Hydroxide Monohydrate LiOH 56.5%min Delivered China RMB/mt
Minor Metals,Lithium,"Lithium Hydroxide Monohydrate LiOH 56.5%min, Magnets 0.0001%max Delivered China RMB/mt"
Minor Metals,Lithium,"Lithium Hydroxide Monohydrate LiOH 56.5%min, Magnets 0.0001%max Delivered South Korea USD/kg"
Minor Metals,Lithium,"Lithium Hydroxide Monohydrate LiOH 56.5%min, Magnets 0.0001%max FOB China USD/kg"
Minor Metals,Lithium,Lithium Iron Phosphate Li 3.9%min Delivered China RMB/mt
Minor Metals,Lithium,Lithium Manganate Mn 58% Delivered China RMB/mt
Minor Metals,Lithium,Lithium Metal 99%min Delivered China RMB/mt
Minor Metals,Lithium,Lithium Metal 99.9%min Delivered China RMB/mt
Minor Metals,Lithium,Spodumene Li2O 6%min CIF China USD/mt
Minor Metals,Magnesium,Magnesium Alloy AM50A EXW China RMB/mt
Minor Metals,Magnesium,Magnesium Alloy AM50A FOB China USD/mt
Minor Metals,Magnesium,Magnesium Alloy AM60B EXW China RMB/mt
Minor Metals,Magnesium,Magnesium Alloy AM60B FOB China USD/mt
Minor Metals,Magnesium,Magnesium Alloy AZ91D EXW China RMB/mt
Minor Metals,Magnesium,Magnesium Alloy AZ91D FOB China USD/mt
Minor Metals,Magnesium,Magnesium Ingot 99.9%min EXW China RMB/mt
Minor Metals,Magnesium,Magnesium Ingot 99.9%min Ex-VAT FOB China USD/mt
Minor Metals,Magnesium,Magnesium Ingot 99.9%min FOB China USD/mt
Minor Metals,Magnesium,Magnesium Ingot 99.9%min In port India INR/kg
Minor Metals,Magnesium,Magnesium Ingot 99.9%min In warehouse Rotterdam USD/mt
Minor Metals,Magnesium,Magnesium Ingot 99.9%min in warehouse Russia RUB/kg
Minor Metals,Magnesium,Magnesium Ingot 99.95%min EXW China RMB/mt
Minor Metals,Magnesium,Magnesium Powder 99.9%min EXW China RMB/mt
Minor Metals,Magnesium,Magnesium Powder 99.9%min FOB China USD/mt
Minor Metals,Manganese,Manganese Briquette 97%min FOB China USD/mt
Minor Metals,Manganese,Manganese Dioxide Alkaline 91%min EXW China RMB/mt
Minor Metals,Manganese,Manganese Dioxide Carbon 91%min EXW China RMB/mt
Minor Metals,Manganese,Manganese Flake 99.7%min CIF India USD/mt
Minor Metals,Manganese,Manganese Flake 99.7%min EXW China RMB/mt
Minor Metals,Manganese,Manganese Flake 99.7%min Ex-VAT FOB China USD/mt
Minor Metals,Manganese,Manganese Flake 99.7%min FOB China USD/mt
Minor Metals,Manganese,Manganese Flake 99.7%min In warehouse Rotterdam USD/mt
Minor Metals,Manganese,Manganese Lump 96.5%min FOB China USD/mt
Minor Metals,Manganese,Manganese Lump 96.5%min In warehouse Russia RUB/kg
Minor Metals,Manganese,Manganese Sulfate Mn 32%min EXW China RMB/t
Minor Metals,Mercury,Mercury Metal 99.9%min Ex-VAT EXW China RMB/kg
Minor Metals,Mercury,Mercury Metal 99.999%min Ex-VAT EXW China RMB/kg
Minor Metals,Molybdenum,Ammonium Heptamolybdate 54%min EXW China RMB/mt
Minor Metals,Molybdenum,Ammonium Tetrathiomolybdate 56%min EXW China RMB/mt
Minor Metals,Molybdenum,Molybdenum Bar 99.9%min EXW China RMB/kg
Minor Metals,Molybdenum,Molybdenum Conc. 45%min EXW China RMB/mtu
Minor Metals,Molybdenum,Molybdenum Oxide 57%min In port India USD/lb
Minor Metals,Molybdenum,Molybdenum Oxide 50%min EXW China RMB/mtu
Minor Metals,Molybdenum,Molybdenum Oxide 57%min CIF China USD/lb Mo
Minor Metals,Molybdenum,Molybdenum Oxide 57%min In port South Korea USD/lb
Minor Metals,Molybdenum,Molybdenum Oxide 57%min In warehouse Rotterdam USD/lb
Minor Metals,Molybdenum,Molybdenum Oxide 99.9%min EXW China RMB/mt
Minor Metals,Molybdenum,Molybdenum Powder 99.95%min EXW China RMB/kg
Minor Metals,Molybdenum,Molybdenum Scrap Cutting Scrap 99.95%min Ex-VAT EXW China RMB/kg
Minor Metals,Molybdenum,Molybdenum Scrap Wire Scrap 99.95%min Ex-VAT EXW China RMB/kg
Minor Metals,Molybdenum,Sodium Molybdate 99%min EXW China RMB/mt
Minor Metals,Niobium,"Niobium Conc. Nb2O5 50%min, Ta2O5 5%min, CIF China USD/lb Oxides"
Minor Metals,Niobium,Niobium Pentoxide 99.5%min EXW China RMB/kg
Minor Metals,Niobium,Niobium Pentoxide 99.5%min FOB China USD/kg
Minor Metals,Niobium,Niobium Pentoxide 99.99%min EXW China RMB/kg
Minor Metals,Niobium,Niobium Pentoxide 99.99%min FOB China USD/kg
Minor Metals,Rhenium,Rhenium APR 99.99%min EXW China RMB/kg
Minor Metals,Selenium,Selenium Dioxide 98%min Delivered China RMB/kg
Minor Metals,Selenium,Selenium Powder 99.5%min In warehouse Rotterdam USD/lb
Minor Metals,Selenium,Selenium Powder 99.9%min CIF China USD/lb
Minor Metals,Selenium,Selenium Powder 99.9%min Delivered China RMB/kg
Minor Metals,Selenium,Selenium Powder 99.9%min Delivered India INR/kg
Minor Metals,Selenium,Selenium Powder 99.9%min Delivered US USD/lb
Minor Metals,Selenium,Selenium Powder 99.9%min FOB Russia USD/kg
Minor Metals,Selenium,Selenium Powder 99.95%min Delivered China RMB/kg
Minor Metals,Silicon,Polysilicon 9N Delivered China RMB/kg
Minor Metals,Silicon,Silicon Metal 2-2-02 Delivered China RMB/mt
Minor Metals,Silicon,Silicon Metal 2-2-02 Ex-VAT FOB China USD/mt
Minor Metals,Silicon,Silicon Metal 2-2-02 FOB China USD/mt
Minor Metals,Silicon,Silicon Metal 3-3-03 Delivered China RMB/mt
Minor Metals,Silicon,Silicon Metal 3-3-03 Ex-VAT FOB China USD/mt
Minor Metals,Silicon,Silicon Metal 3-3-03 FOB China USD/mt
Minor Metals,Silicon,Silicon Metal 3-3-03 P 50ppm max Delivered China RMB/mt
Minor Metals,Silicon,Silicon Metal 3-3-03 P 50ppm max Ex-VAT FOB China USD/mt
Minor Metals,Silicon,Silicon Metal 3-3-03 P 50ppm max FOB China USD/mt
Minor Metals,Silicon,Silicon Metal 4-2-1 Delivered China RMB/mt
Minor Metals,Silicon,Silicon Metal 4-4-1 Delivered China RMB/mt
Minor Metals,Silicon,Silicon Metal 4-4-1 Delivered Europe EUR/mt
Minor Metals,Silicon,Silicon Metal 4-4-1 Ex-VAT FOB China USD/mt
Minor Metals,Silicon,Silicon Metal 4-4-1 FOB China USD/mt
Minor Metals,Silicon,Silicon Metal 4-4-1 In port India INR/kg
Minor Metals,Silicon,Silicon Metal 5-5-3 Delivered China RMB/mt
Minor Metals,Silicon,Silicon Metal 5-5-3 Delivered Europe EUR/mt
Minor Metals,Silicon,Silicon Metal 5-5-3 Ex-VAT FOB China USD/mt
Minor Metals,Silicon,Silicon Metal 5-5-3 FOB China USD/mt
Minor Metals,Silicon,Silicon Metal 5-5-3 In port India INR/kg
Minor Metals,Silicon,Silicon Metal 98.5%min Delivered US USD/lb
Minor Metals,Silicon,Silicon Powder 99%min Delivered China RMB/mt
Minor Metals,Strontium,Strontium Metal 99%min EXW China RMB/mt
Minor Metals,Strontium,Strontium Metal 99%min FOB China USD/mt
Minor Metals,Tantalum,Tantalum Conc. Ta2O5 30%min CIF China USD/lb Ta2O5
Minor Metals,Tantalum,Tantalum Conc. Ta2O5 30%min in warehouse Rotterdam USD/lb Ta2O5
Minor Metals,Tantalum,Tantalum Metal 99.95%min Delivered US USD/kg
Minor Metals,Tantalum,Tantalum Metal 99.95%min EXW China RMB/kg
Minor Metals,Tantalum,Tantalum Metal 99.95%min FOB China USD/kg
Minor Metals,Tantalum,Tantalum Pentoxide 99.5%min EXW China RMB/kg
Minor Metals,Tantalum,Tantalum Pentoxide 99.5%min FOB China USD/kg
Minor Metals,Tantalum,Tantalum Pentoxide 99.99%min EXW China RMB/kg
Minor Metals,Tantalum,Tantalum Pentoxide 99.99%min FOB China USD/kg
Minor Metals,Tellurium,Tellurium Metal 99.9%min In warehouse Rotterdam USD/kg
Minor Metals,Tellurium,Tellurium Metal 99.99%min EXW China RMB/kg
Minor Metals,Tellurium,Tellurium Metal 99.99%min In warehouse Rotterdam USD/kg
Minor Metals,Titanium,"Ilmenite Conc Australian TiO2 55-58%min, Fe 27%min CIF China USD/mt"
Minor Metals,Titanium,"Ilmenite Conc Kenyan TiO2 48%min, Fe2O3 20%max CIF China USD/mt"
Minor Metals,Titanium,"Ilmenite Conc Mozambican TiO2 52%min, Fe2O3 25%max CIF China USD/mt"
Minor Metals,Titanium,Ilmenite Conc TiO2 46%min; Fe2O3 8%max; Ex-VAT EXW China RMB/mt
Minor Metals,Titanium,Ilmenite Conc TiO2 50%min; Fe 30%min; In port China RMB/mt
Minor Metals,Titanium,Ilmenite Conc TiO2 50%min; Fe2O3 14%max; In port China RMB/mt
Minor Metals,Titanium,"Ilmenite Conc TiO2 52%min, Fe2O3 25%max FOB Vietnam USD/mt"
Minor Metals,Titanium,Reduced Ilmenite TiO2 56%min EXW China RMB/mt
Minor Metals,Titanium,Rutile Conc TiO2 95%min In port China RMB/mt
Minor Metals,Titanium,Titanium Dioxide A TiO2 98%min Delivered China RMB/mt
Minor Metals,Titanium,Titanium Dioxide R TiO2 93%min Delivered China RMB/mt
Minor Metals,Titanium,Titanium Dioxide R TiO2 93%min FOB China USD/mt
Minor Metals,Titanium,Titanium Pipe TA2 25.4x1mm EXW China RMB/kg
Minor Metals,Titanium,Titanium Plate TA2 2mm EXW China RMB/kg
Minor Metals,Titanium,Titanium Slag TiO2 74%min EXW China RMB/mt
Minor Metals,Titanium,Titanium Slag TiO2 90%min EXW China RMB/mt
Minor Metals,Titanium,Titanium Sponge 99.7%min EXW China RMB/mt
Minor Metals,Titanium,Titanium Tetrachloride 99.99%min EXW China RMB/mt
Minor Metals,Tungsten,Sodium Tungstate WO3 68%min EXW China RMB/mt
Minor Metals,Tungsten,Thorium Tungsten Electrode ThO2 1.7-2.2% EXW China RMB/kg
Minor Metals,Tungsten,Tungsten APT 88.5%min EXW China RMB/mt
Minor Metals,Tungsten,Tungsten APT 88.5%min FOB China USD/mtu
Minor Metals,Tungsten,Tungsten APT 88.5%min In warehouse Rotterdam USD/mtu
Minor Metals,Tungsten,Tungsten Bar W-4 99.9%min EXW China RMB/kg
Minor Metals,Tungsten,Tungsten Bar W-4 99.9%min FOB China USD/kg
Minor Metals,Tungsten,Tungsten Carbide 99.8%min 2.5-7.0?m EXW China RMB/kg
Minor Metals,Tungsten,Tungsten Carbide 99.8%min 2.5-7.0?m FOB China USD/kg
Minor Metals,Tungsten,Tungsten Ore WO3 50%min FOB Africa USD/mtu
Minor Metals,Tungsten,Tungsten Ore WO3 65%min EXW China RMB/mt
Minor Metals,Tungsten,Tungsten Oxide WO3 99.95%min EXW China RMB/mt
Minor Metals,Tungsten,Tungsten Oxide WO3 99.95%min FOB China USD/mtu
Minor Metals,Tungsten,Tungsten Powder 99.95%min 2.5-7.0?m EXW China RMB/kg
Minor Metals,Tungsten,Tungsten Scrap Anvil 90%min Ex-VAT Delivered China RMB/kg
Minor Metals,Tungsten,Tungsten Scrap CNC Cutting Blades 70%min Ex-VAT Delivered China RMB/kg
Minor Metals,Tungsten,Tungsten Scrap Drills 70%min Ex-VAT Delivered China RMB/kg
Minor Metals,Tungsten,"Tungsten Scrap Grinding Swarf W 70%min, Co 5%min Ex-VAT Delivered China RMB/mtu"
Minor Metals,Vanadium,Ammonium Metavanadate 98%min EXW China RMB/mt
Minor Metals,Vanadium,Vanadium Nitride N 14%min EXW China RMB/mt
Minor Metals,Vanadium,Vanadium Pentoxide Flake 92%min CIF China USD/lb V2O5
Minor Metals,Vanadium,Vanadium Pentoxide Flake 92%min In port China RMB/mt
Minor Metals,Vanadium,Vanadium Pentoxide Flake 98%min CIF China USD/lb V2O5
Minor Metals,Vanadium,Vanadium Pentoxide Flake 98%min EXW China RMB/mt
Minor Metals,Vanadium,Vanadium Pentoxide Flake 98%min In port China RMB/mt
Minor Metals,Vanadium,Vanadium Pentoxide Flake 98%min In warehouse Rotterdam USD/lb V2O5
Minor Metals,Vanadium,Vanadium Pentoxide Powder 98%min EXW China RMB/mt
Minor Metals,Vanadium,Vanadium Pentoxide Powder 99%min EXW China RMB/mt
Minor Metals,Vanadium,Vanadium Pentoxide Powder 99.5%min EXW China RMB/mt
Minor Metals,Zirconium,Fused Zirconia Zr(Hf)O2 98.5%min EXW China RMB/mt
Minor Metals,Zirconium,Zircon Sand Australian Zr(Hf)O2 66%min In port China RMB/mt
Minor Metals,Zirconium,Zircon Sand Australian Zr(Hf)O2 66%min In port China USD/mt
Minor Metals,Zirconium,Zircon Sand Indonesian Zr(Hf)O2 66%min CIF China USD/mt
Minor Metals,Zirconium,Zircon Sand S.A. Zr(Hf)O2 65%min In port China RMB/mt
Minor Metals,Zirconium,Zircon Sand S.A. Zr(Hf)O2 65%min In port China USD/mt
Minor Metals,Zirconium,Zircon Sand S.A. Zr(Hf)O2 66%min In port China USD/mt
Minor Metals,Zirconium,Zircon Sand Zr(Hf)O2 65%min EXW China RMB/mt
Minor Metals,Zirconium,Zirconium Carbonate Zr(Hf)O2 40%min EXW China RMB/mt
Minor Metals,Zirconium,"Zirconium Carbonate Zr(Hf)O2 40%min, Cl 50ppm EXW China RMB/mt"
Minor Metals,Zirconium,Zirconium Oxychloride Zr(Hf)O2 36%min EXW China RMB/mt
Minor Metals,Zirconium,Zirconium Silicate Zr(Hf)O2 64.5%min EXW China RMB/mt
Minor Metals,Zirconium,Zirconium Sponge Zr+Hf 99.4%min EXW China RMB/kg
Rare Earths,Lutetium,Lutetium Oxide 99.99%min China RMB/kg
Rare Earths,Neodymium,"Cerium Carbonate TREO 45%min, CeO2/REO 100% EXW China RMB/mt"
Rare Earths,Neodymium,"Cerium Carbonate TREO 45%min, CeO2/REO 100% FOB China USD/mt"
Rare Earths,Neodymium,Cerium Metal 99%min EXW China RMB/mt
Rare Earths,Neodymium,Cerium Metal 99%min FOB China USD/kg
Rare Earths,Neodymium,Cerium Oxide 99%min FOB China USD/mt
Rare Earths,Neodymium,Cerium Oxide 99.9%min EXW China RMB/mt
Rare Earths,Neodymium,Cerium Oxide 99.9%min In warehouse Rotterdam USD/kg
Rare Earths,Neodymium,Cerium Oxide 99.99%min EXW China RMB/mt
Rare Earths,Neodymium,"LaCe Carbonate TREO 45%min, CeO2/REO 65±2% EXW China RMB/mt"
Rare Earths,Neodymium,"LaCe Chloride TREO 45%min, CeO2/REO 65±2% EXW China RMB/mt"
Rare Earths,Neodymium,NdFeB Sintered Rough 35H EXW China RMB/kg
Rare Earths,Neodymium,NdFeB Sintered Rough 35M EXW China RMB/kg
Rare Earths,Neodymium,NdFeB Sintered Rough 45H EXW China RMB/kg
Rare Earths,Neodymium,NdFeB Sintered Rough 45M EXW China RMB/kg
Rare Earths,Neodymium,NdFeB Sintered Rough 48H EXW China RMB/kg
Rare Earths,Neodymium,NdFeB Sintered Rough 50H EXW China RMB/kg
Rare Earths,Neodymium,NdFeB Sintered Rough 50M EXW China RMB/kg
Rare Earths,Neodymium,NdFeB Sintered Rough N35 EXW China RMB/kg
Rare Earths,Neodymium,NdFeB Sintered Rough N45 EXW China RMB/kg
Rare Earths,Neodymium,NdFeB Sintered Rough N52 EXW China RMB/kg
Rare Earths,Neodymium,Neodymium Metal 99%min EXW China RMB/mt
Rare Earths,Neodymium,Neodymium Metal 99%min FOB China USD/kg
Rare Earths,Neodymium,Neodymium Oxide 99.5%min EXW China RMB/mt
Rare Earths,Neodymium,Neodymium Oxide 99.5%min FOB China USD/mt
Rare Earths,Neodymium,"PrNd Mischmetal Pr 25%, Nd 75% EXW China RMB/mt"
Rare Earths,Neodymium,"PrNd Mischmetal Pr 25%, Nd 75% FOB China USD/kg"
Rare Earths,Neodymium,"PrNd Oxide Pr6O11 25%, Nd2O3 75% EXW China RMB/mt"
Rare Earths,Praseodymium,Praseodymium Metal 99.5%min FOB China USD/kg
Rare Earths,Praseodymium,Praseodymium Oxide 99.5%min EXW China RMB/mt
Rare Earths,Praseodymium,Praseodymium Oxide 99.5%min FOB China USD/mt
Rare Earths,Praseodymium,Praseodymium Oxide 99.5%min In warehouse Rotterdam USD/kg
Rare Earths,Samarium,Samarium Cobalt 30H EXW China RMB/kg
Rare Earths,Samarium,Samarium Metal 99.5%min EXW China RMB/kg
Rare Earths,Samarium,Samarium Metal 99.5%min FOB China USD/kg
Rare Earths,Samarium,Samarium Oxide 99.9%min EXW China RMB/mt
Rare Earths,Samarium,Samarium Oxide 99.9%min FOB China USD/mt
Rare Earths,Samarium,"SmEuGd Oxide TREO 220g/L min, Eu2O3/REO 8%min EXW China RMB/mt"
Rare Earths,Scandium,Scandium Aluminum Sc 2% EXW China RMB/kg
Rare Earths,Scandium,Scandium Metal 99.99%min EXW China RMB/kg
Rare Earths,Scandium,Scandium Metal 99.999%min EXW China RMB/kg
Rare Earths,Scandium,Scandium Oxide 99.99%min EXW China RMB/kg
Rare Earths,Terbium,Terbium Metal 99.9%min EXW China RMB/kg
Rare Earths,Terbium,Terbium Metal 99.9%min FOB China USD/kg
Rare Earths,Terbium,Terbium Oxide 99.99%min EXW China RMB/kg
Rare Earths,Terbium,Terbium Oxide 99.99%min FOB China USD/kg
Rare Earths,Ytterbium,Ytterbium Oxide 99.99%min EXW China RMB/kg
Rare Earths,Yttrium,YEu Oxide Eu2O3/REO 6.6%min EXW China RMB/kg
Rare Earths,Yttrium,YEu Oxide Eu2O3/REO 6.6%min FOB China USD/kg
Rare Earths,Yttrium,Yttrium Metal 99.9%min EXW China RMB/kg
Rare Earths,Yttrium,Yttrium Metal 99.9%min FOB China USD/kg
Rare Earths,Yttrium,Yttrium Oxide 99.999%min EXW China RMB/mt
Rare Earths,Yttrium,Yttrium Oxide 99.999%min FOB China USD/kg
Rare Earths,Yttrium,Yttrium Oxide 99.999%min In warehouse Rotterdam USD/kg
Refractories,Calcined Bauxite,Calcined Bauxite 85%min 1-3mm EXW China RMB/mt
Refractories,Calcined Bauxite,Calcined Bauxite 86%min 1-3mm FOB China USD/mt
Refractories,Calcined Bauxite,Calcined Bauxite 87%min 1-3mm EXW China RMB/mt
Refractories,Carbon,Calcined Pet Coke S 3%max EXW China RMB/mt
Refractories,Carbon,Coal Tar Pitch Coking Value 53%min EXW China RMB/mt
Refractories,Carbon,Graphite Electrode H.P. D300mm FOB China USD/mt
Refractories,Carbon,Graphite Electrode H.P. D400mm EXW China RMB/mt
Refractories,Carbon,Graphite Electrode H.P. D450mm FOB China USD/mt
Refractories,Carbon,Graphite Electrode H.P. D500mm EXW China RMB/mt
Refractories,Carbon,Graphite Electrode R.P. D400mm EXW China RMB/mt
Refractories,Carbon,Graphite Electrode U.H.P. D500mm EXW China RMB/mt
Refractories,Carbon,Graphite Electrode U.H.P. D500mm FOB China USD/mt
Refractories,Carbon,Graphite Electrode U.H.P. D600mm EXW China RMB/mt
Refractories,Carbon,Graphite Electrode U.H.P. D600mm FOB China USD/mt
Refractories,Carbon,Pet Coke S 2%max EXW China RMB/mt
Refractories,Carbon,Pet Coke S 3%max EXW China RMB/mt
Refractories,Carbon,Prebaked Anode S 3%max EXW China RMB/mt
Refractories,Carbon,Prebaked Anode S 3%max FOB China USD/mt
Refractories,Fused Alumina,Calcined Alumina 99%min 1-3mm US USD/st
Refractories,Fused Alumina,Fused Alumina Brown 95%min 1-3mm EXW China RMB/mt
Refractories,Fused Alumina,Fused Alumina Brown 95%min 1-3mm EXW India INR/mt
Refractories,Fused Alumina,Fused Alumina Brown 95%min 1-3mm FOB China USD/mt
Refractories,Fused Alumina,Fused Alumina Brown 95%min 1-3mm In warehouse Rotterdam EUR/mt
Refractories,Fused Alumina,Fused Alumina Brown 95%min 1-3mm US USD/st
Refractories,Fused Alumina,Fused Alumina Brown 95%min F36 EXW China RMB/mt
Refractories,Fused Alumina,Fused Alumina Brown 95%min F36 EXW India INR/mt
Refractories,Fused Alumina,Fused Alumina Brown 95%min F36 FOB China USD/mt
Refractories,Fused Alumina,Fused Alumina White 99%min 1-3mm EXW China RMB/mt
Refractories,Fused Alumina,Fused Alumina White 99%min 1-3mm FOB China USD/mt
Refractories,Fused Alumina,Fused Alumina White 99%min 1-3mm In warehouse Rotterdam EUR/mt
Refractories,Fused Alumina,Fused Alumina White 99%min 1-3mm US USD/st
Refractories,Fused Alumina,Fused Alumina White 99%min F36 EXW China RMB/mt
Refractories,Fused Alumina,Fused Alumina White 99%min F36 FOB China USD/mt
Refractories,Fused Alumina,Tabular Alumina 99%min 1-3mm US USD/st
Refractories,Graphite,Amorphous Graphite 75%min 0-3mm EXW China RMB/mt
Refractories,Graphite,Amorphous Graphite 80%min 20-50mm EXW China RMB/mt
Refractories,Graphite,Amorphous Graphite 80%min 200mesh min EXW China RMB/mt
Refractories,Graphite,Expansible Graphite 99 80 250 EXW China RMB/mt
Refractories,Graphite,Flake Graphite +195 EXW China RMB/mt
Refractories,Graphite,Flake Graphite +895 EXW China RMB/mt
Refractories,Graphite,Flake Graphite -190 EXW China RMB/mt
Refractories,Graphite,Flake Graphite -194 EXW China RMB/mt
Refractories,Graphite,Flake Graphite -194 FOB China USD/mt
Refractories,Graphite,Flake Graphite -199 EXW China RMB/mt
Refractories,Graphite,Spherical Graphite 99.95%min 17?m max EXW China RMB/mt
Refractories,Magnesia,Magnesia C.C. 85%min 200mesh EXW China RMB/mt
Refractories,Magnesia,Magnesia C.C. 90%min 200mesh EXW China RMB/mt
Refractories,Magnesia,Magnesia C.C. 90%min 200mesh FOB China USD/mt
Refractories,Magnesia,Magnesia C.C. 97%min 0-5mm EXW China RMB/mt
Refractories,Magnesia,Magnesia D.B. 90%min 3-15mm EXW China RMB/mt
Refractories,Magnesia,Magnesia D.B. 90%min 3-15mm FOB China USD/mt
Refractories,Magnesia,Magnesia D.B. 90%min 3-15mm In warehouse Rotterdam EUR/mt
Refractories,Magnesia,Magnesia D.B. 90%min 3-15mm US USD/st
Refractories,Magnesia,Magnesia D.B. 95%min 0-30mm EXW China RMB/mt
Refractories,Magnesia,Magnesia D.B. 95%min 0-30mm FOB China USD/mt
Refractories,Magnesia,Magnesia D.B. 95%min 0-30mm In warehouse Rotterdam EUR/mt
Refractories,Magnesia,Magnesia D.B. 96.5%min 0-30mm EXW China RMB/mt
Refractories,Magnesia,Magnesia D.B. 97%min 0-30mm EXW China RMB/mt
Refractories,Magnesia,Magnesia D.B. 97%min 0-30mm FOB China USD/mt
Refractories,Magnesia,Magnesia E.F. 97%min EXW China RMB/mt
Refractories,Magnesia,Magnesia E.F. 97%min H-Ca EXW China RMB/mt
Refractories,Magnesia,Magnesia E.F. 97%min H-Ca FOB China USD/mt
Refractories,Magnesia,Magnesia E.F. 97.5%min Large Crystal EXW China RMB/mt
Refractories,Magnesia,Magnesia E.F. 97.5%min Large Crystal FOB China USD/mt
Refractories,Magnesia,Magnesite Lump 47%min EXW China RMB/mt
Refractories,Magnesia,Magnesite Pellet 47%min EXW China RMB/mt
Refractories,Silicon Carbide,Silicon Carbide Black 88%min 0-10mm EXW China RMB/mt
Refractories,Silicon Carbide,Silicon Carbide Black 88%min 0-10mm FOB China USD/mt
Refractories,Silicon Carbide,Silicon Carbide Black 98%min 10-30cm EXW China RMB/mt
Refractories,Silicon Carbide,Silicon Carbide Black 98%min F16-100 EXW China RMB/mt
Refractories,Silicon Carbide,Silicon Carbide Black 98%min F16-100 FOB China USD/mt
Refractories,Silicon Carbide,Silicon Carbide Green 98%min F240 EXW China RMB/mt
Refractories,Silicon Carbide,Silicon Carbide Green 98%min F240 FOB China USD/mt
Refractories,Silicon Carbide,Silicon Carbide Green 98%min JIS2000 EXW China RMB/mt
Refractories,Silicon Carbide,Silicon Carbide Green 98%min JIS2000 FOB China USD/mt
Refractories,Silicon Carbide,Silicon Carbide Green 98.5%min 10-30cm EXW China RMB/mt
Refractories,Silicon Carbide,Silicon Carbide Black 88%min 0-10mm In warehouse Rotterdam EUR/mt
Stainless & Special,Bearing Steel,Cold Heading Wire 22A ?6.5mm In warehouse Shanghai RMB/mt
Stainless & Special,Bearing Steel,Electrical Steel Grain Oriented 130 0.3*980mm In warehouse China RMB/mt
Stainless & Special,Bearing Steel,Electrical Steel Non-grain 470 0.5*1200mm In warehouse China RMB/mt
Stainless & Special,Bearing Steel,"Electrical Steel Non-grain 800 0.5*1,200mm In warehouse China RMB/mt"
Stainless & Special,Bearing Steel,Stainless Bar 321 60mm In warehouse Dainan RMB/mt
Stainless & Special,Bearing Steel,Stainless CR Coil 201/2B 1.0mm In warehouse China RMB/mt
Stainless & Special,Bearing Steel,Stainless CR Coil 304/2B 1.0mm In warehouse China RMB/mt
Stainless & Special,Bearing Steel,Stainless CR Coil 430/2B 1.0mm In warehouse China RMB/mt
Stainless & Special,Bearing Steel,Stainless HR Coil 201/No.1 6.0mm In warehouse China RMB/mt
Stainless & Special,Bearing Steel,Stainless HR Coil 304/No.1 6.0mm In warehouse China RMB/mt
Stainless & Special,Stainless Bar,Stainless Bar 304 60mm In warehouse Dainan RMB/mt
Stainless & Special,Stainless Bar,Stainless Bar 321 60mm In warehouse Dainan RMB/mt
Stainless & Special,Stainless Pipe,Stainless Seamless Pipe 304 108*4mm In warehouse Wenzhou RMB/mt
Stainless & Special,Stainless Scrap,Stainless Scrap 304 Solid In warehouse Tokyo JPY/mt
Stainless & Special,Stainless Scrap,Stainless Scrap 304 Solid In warehouse Dainan RMB/mt
Stainless & Special,Structural Steel,Stainless CR Coil 304/2B 1.0mm In warehouse China RMB/mt
Steel Raw Materials,Coal,"Coking Coal A 10%max, S 0.8%max EXW Hebei RMB/mt"
Steel Raw Materials,Coal,"Coking Coal A 10%max, S 0.8%max EXW Henan RMB/mt"
Steel Raw Materials,Coal,"Coking Coal A 10.5%max, S 0.8%max EXW Shanxi RMB/mt"
Steel Raw Materials,Coal,Thermal Coal Q5000 In port Qinhuangdao RMB/mt
Steel Raw Materials,Coal,Thermal Coal Q5500 In port Qinhuangdao RMB/mt
Steel Raw Materials,Coke,"Met. Coke A 13%max, S 0.7%max EXW Hebei RMB/mt"
Steel Raw Materials,Coke,"Met. Coke A 13%max, S 0.7%max EXW Shandong RMB/mt"
Steel Raw Materials,Coke,"Met. Coke A 13%max, S 0.7%max EXW Shanxi RMB/mt"
Steel Raw Materials,Iron,Cast Iron Z18-22 EXW China RMB/mt
Steel Raw Materials,Iron,Pig Iron L8-10 EXW China RMB/mt
Steel Raw Materials,Iron,S.G. Cast Iron Q10-12 EXW China RMB/mt
Steel Raw Materials,Iron Ore,Iron Ore Fine 65%min EXW Benxi RMB/dt
Steel Raw Materials,Iron Ore,Iron Ore Fine 65%min EXW Laiwu RMB/dt
Steel Raw Materials,Iron Ore,Iron Ore Fine 65%min Ex-VAT EXW Daixian RMB/wt
Steel Raw Materials,Iron Ore,Iron Ore Fine 66%min Ex-VAT EXW Handan RMB/dt
Steel Raw Materials,Iron Ore,Iron Ore Fine Australian 58%min In port China RMB/wt
Steel Raw Materials,Iron Ore,Iron Ore Fine Australian 62%min CNF China USD/dt
Steel Raw Materials,Iron Ore,Iron Ore Fine Australian 62%min In port China RMB/wt
Steel Raw Materials,Iron Ore,Iron Ore Fine Brazilian 65%min In port China RMB/wt
Steel Raw Materials,Iron Ore,Iron Ore Lump Australian 62%min In port China RMB/wt
Steel Raw Materials,Steel Billet,Steel Billet HRB335 150x150mm EXW China RMB/mt
Steel Raw Materials,Steel Billet,Steel Billet Q235 150x150mm EXW China RMB/mt
Steel Raw Materials,Steel Scrap,Steel Scrap HMS 4-6mm EXW Shandong RMB/mt
Steel Raw Materials,Steel Scrap,Steel Scrap HMS 8mm min EXW Wuxi RMB/mt
Steel Raw Materials,Steel Scrap,Steel Scrap MMS 4-6mm EXW Guangzhou RMB/mt
Steel Raw Materials,Steel Scrap,Steel Scrap MMS 4-6mm EXW Tianjin RMB/mt
Steel Raw Materials,Steel Scrap,Steel Scrap MMS 6-10mm EXW Liaoning RMB/mt
Steel Raw Materials,Steel Scrap,Steel Scrap Process Scrap Delivered China RMB/mt
1 group category title
2 Base Metals Copper Blister Copper TC 98.5%min Delivered China RMB/mt
3 Base Metals Copper Copper Cathode 99.99%min Delivered China RMB/mt
4 Base Metals Copper Copper Conc. 20%min Delivered China RMB/mt Cu
5 Base Metals Copper Copper Conc. TC 25%min CIF China USD/mt
6 Base Metals Copper Copper Rod TC ?8mm EXW China RMB/mt
7 Base Metals Copper Copper Scrap Wire 99%min Delivered China RMB/mt
8 Base Metals Lead Lead Conc. 60%min EXW China RMB/mt Pb
9 Base Metals Lead Lead Conc. TC 50%min CIF China USD/mt
10 Base Metals Lead Lead Ingot 99.994%min In warehouse Guangzhou RMB/mt
11 Base Metals Lead Lead Ingot 99.994%min In warehouse Shanghai RMB/mt
12 Base Metals Lead Lead Ingot Secondary 98%min EXW China RMB/mt
13 Base Metals Lead Lead Ingot Secondary 98%min Ex-VAT EXW China RMB/mt
14 Base Metals Lead Lead Ingot Secondary 99.994%min EXW China RMB/mt
15 Base Metals Lead Lead Ingot Secondary 99.994%min Ex-VAT EXW China RMB/mt
16 Base Metals Lead Lead Scrap Recycled Deep Cycle Battery Ex-VAT Delivered China RMB/mt
17 Base Metals Lead Lead Scrap Recycled Start-type Battery Ex-VAT Delivered China RMB/mt
18 Base Metals Nickel Nickel Cathode 99.96%min EXW China RMB/mt
19 Base Metals Nickel Nickel Cathode Norilsk 99.96%min In port China RMB/mt
20 Base Metals Nickel Nickel Ore Philippine 0.8%min CIF China USD/mt
21 Base Metals Nickel Nickel Ore Philippine 0.8%min In port China RMB/mt
22 Base Metals Nickel Nickel Ore Philippine 1.3%min CIF China USD/mt
23 Base Metals Nickel Nickel Ore Philippine 1.5%min CIF China USD/mt
24 Base Metals Nickel Nickel Ore Philippine 1.5%min In port China RMB/mt
25 Base Metals Nickel Nickel Ore Philippine 1.8%min CIF China USD/mt
26 Base Metals Nickel Nickel Sulfate Ni 22%min; Co 0.05%max Delivered China RMB/mt
27 Base Metals Nickel Nickel Sulfate Ni 22%min; Co 0.4%max Delivered China RMB/mt
28 Base Metals Tin Tin Conc. 60%min Delivered China RMB/mtm
29 Base Metals Tin Tin Conc. Burmese 20%min In warehouse MengA RMB/mtm
30 Base Metals Tin Tin Conc. Burmese 30%min In warehouse MengA RMB/mtm
31 Base Metals Tin Tin Ingot 99.9%min EXW China RMB/mt
32 Base Metals Tin Tin Solder Wire Sn 99%min, Ag 0.3%min, Cu 0.7%max EXW China RMB/kg
33 Base Metals Tin Tin Solder Wire Sn 99%min, Cu 0.7%max EXW China RMB/kg
34 Base Metals Zinc Zinc Conc. 50%min EXW North China RMB/mt Zn
35 Base Metals Zinc Zinc Conc. TC 50% CIF China USD/mt
36 Carbon Steel Coated HGI Coil DX51D 0.5mm FOB China USD/mt
37 Carbon Steel Coated HGI Coil DX51D 0.5mm In warehouse Guangzhou RMB/mt
38 Carbon Steel Coated HGI Coil DX51D 0.5mm In warehouse Shanghai RMB/mt
39 Carbon Steel Coated HGI Coil DX51D 0.5mm In warehouse Tianjin RMB/mt
40 Carbon Steel Coated HGI Coil DX51D 1.0mm FOB China USD/mt
41 Carbon Steel Coated HGI Coil DX51D 1.0mm In warehouse Guangzhou RMB/mt
42 Carbon Steel Coated HGI Coil DX51D 1.0mm In warehouse Shanghai RMB/mt
43 Carbon Steel Coated HGI Coil DX51D 1.0mm In warehouse Tianjin RMB/mt
44 Carbon Steel Coated HGI Coil IS513 0.5mm In warehouse Mumbai INR/mt
45 Carbon Steel Cold Rolled Coil CR Coil DC01 1.0mm In warehouse Istanbul USD/mt
46 Carbon Steel Cold Rolled Coil CR Coil IS513 1.0mm In warehouse Mumbai INR/mt
47 Carbon Steel Cold Rolled Coil CR Coil SPCC 0.5mm FOB China USD/mt
48 Carbon Steel Cold Rolled Coil CR Coil SPCC 0.5mm In warehouse Guangzhou RMB/mt
49 Carbon Steel Cold Rolled Coil CR Coil SPCC 0.5mm In warehouse Shanghai RMB/mt
50 Carbon Steel Cold Rolled Coil CR Coil SPCC 0.5mm In warehouse Tianjin RMB/mt
51 Carbon Steel Cold Rolled Coil CR Coil SPCC 1.0mm FOB China USD/mt
52 Carbon Steel Cold Rolled Coil CR Coil SPCC 1.0mm In warehouse Guangzhou RMB/mt
53 Carbon Steel Cold Rolled Coil CR Coil SPCC 1.0mm In warehouse Hanoi VND/mt
54 Carbon Steel Cold Rolled Coil CR Coil SPCC 1.0mm In warehouse Kuala Lumpur MYR/mt
55 Carbon Steel Cold Rolled Coil CR Coil SPCC 1.0mm In warehouse Shanghai RMB/mt
56 Carbon Steel Cold Rolled Coil CR Coil SPCC 1.0mm In warehouse Tianjin RMB/mt
57 Carbon Steel Cold Rolled Coil LaCe Chloride TREO 45%min, CeO2/REO 65±2% EXW China RMB/mt
58 Carbon Steel Cold Rolled Coil LaCe Mischmetal Ce/RE 65%min, Fe 0.3%max EXW China RMB/mt
59 Carbon Steel Cold Rolled Coil Lanthanum Oxide 99.9%min FOB China USD/mt
60 Carbon Steel Cold Rolled Coil Lanthanum Oxide 99.999%min EXW China RMB/mt
61 Carbon Steel Cold Rolled Coil Lanthanum Oxide 99.999%min FOB China USD/kg
62 Carbon Steel Hot Rolled Coil Ferro-gadolinium Gd 73% EXW China RMB/mt
63 Carbon Steel Hot Rolled Coil Ferro-holmium 80% EXW China RMB/kg
64 Carbon Steel Hot Rolled Coil Gadolinium Oxide 99.5%min EXW China RMB/mt
65 Carbon Steel Hot Rolled Coil Gadolinium Oxide 99.99%min EXW China RMB/mt
66 Carbon Steel Hot Rolled Coil HR Coil S275 JR 3.0mm In warehouse Istanbul USD/mt
67 Carbon Steel Hot Rolled Coil HR Coil IS2062 3.0mm In warehouse Mumbai INR/mt
68 Carbon Steel Hot Rolled Coil HR Coil Q235B 2.75mm In warehouse Guangzhou RMB/mt
69 Carbon Steel Hot Rolled Coil HR Coil Q235B 2.75mm In warehouse Shanghai RMB/mt
70 Carbon Steel Hot Rolled Coil HR Coil Q235B 2.75mm In warehouse Tianjin RMB/mt
71 Carbon Steel Hot Rolled Coil HR Coil Q235B 5.5mm In warehouse Guangzhou RMB/mt
72 Carbon Steel Hot Rolled Coil HR Coil Q235B 5.5mm In warehouse Shanghai RMB/mt
73 Carbon Steel Hot Rolled Coil HR Coil Q235B 5.5mm In warehouse Tianjin RMB/mt
74 Carbon Steel Hot Rolled Coil HR Coil SS400 2.75mm FOB China USD/mt
75 Carbon Steel Hot Rolled Coil HR Coil SS400 3.0mm In warehouse Bangkok THB/mt
76 Carbon Steel Hot Rolled Coil HR Coil SS400 3.0mm In warehouse Hanoi VND/mt
77 Carbon Steel Hot Rolled Coil HR Coil SS400 3.0mm In warehouse Kuala Lumpur MYR/mt
78 Carbon Steel Hot Rolled Coil HR Coil SS400 5.5mm FOB China USD/mt
79 Carbon Steel Hot Rolled Coil Holmium Oxide 99.5%min EXW China RMB/kg
80 Carbon Steel Hot Rolled Coil Lanthanum Chloride 99.9%min EXW China RMB/mt
81 Carbon Steel Hot Rolled Coil Lanthanum Metal 99%min EXW China RMB/mt
82 Carbon Steel Hot Rolled Coil Lanthanum Metal 99%min FOB China USD/kg
83 Carbon Steel Hot Rolled Coil Lanthanum Oxide 99.9%min EXW China RMB/mt
84 Carbon Steel Pipe Seamless Pipe 20# ?219*6mm GB8163 In warehouse Guangzhou RMB/mt
85 Carbon Steel Pipe Seamless Pipe 20# ?219*6mm GB8163 In warehouse Shanghai RMB/mt
86 Carbon Steel Pipe Seamless Pipe 20# ?219*6mm GB8163 In warehouse Tianjin RMB/mt
87 Carbon Steel Pipe Welded Pipe Q235 ?114*3.75mm In warehouse Guangzhou RMB/mt
88 Carbon Steel Pipe Welded Pipe Q235 ?114*3.75mm In warehouse Shanghai RMB/mt
89 Carbon Steel Pipe Welded Pipe Q235 ?114*3.75mm In warehouse Tianjin RMB/mt
90 Carbon Steel Plate Medium Plate IS2062 16mm In warehouse Mumbai INR/mt
91 Carbon Steel Plate Medium Plate Q235B 10mm In warehouse Guangzhou RMB/mt
92 Carbon Steel Plate Medium Plate Q235B 10mm In warehouse Shanghai RMB/mt
93 Carbon Steel Plate Medium Plate Q235B 10mm In warehouse Tianjin RMB/mt
94 Carbon Steel Plate Medium Plate Q235B 16mm In warehouse Guangzhou RMB/mt
95 Carbon Steel Plate Medium Plate Q235B 16mm In warehouse Shanghai RMB/mt
96 Carbon Steel Plate Medium Plate Q235B 16mm In warehouse Tianjin RMB/mt
97 Carbon Steel Plate Medium Plate S275 JR 16mm In warehouse Istanbul USD/mt
98 Carbon Steel Plate Medium Plate SS400 10mm FOB China USD/mt
99 Carbon Steel Plate Medium Plate SS400 16mm FOB China USD/mt
100 Carbon Steel Plate Medium Plate SS400 16mm In warehouse Bangkok THB/mt
101 Carbon Steel Plate Medium Plate SS400 16mm In warehouse Hanoi VND/mt
102 Carbon Steel Plate Medium Plate SS400 16mm In warehouse Kuala Lumpur MYR/mt
103 Carbon Steel Rebar Rebar BS4449 GR500 12mm In warehouse Kuala Lumpur MYR/mt
104 Carbon Steel Rebar Rebar BS4449 GR500 16mm In warehouse Kuala Lumpur MYR/mt
105 Carbon Steel Rebar Rebar CB400V/SD390 ?14-32mm In warehouse Hanoi VND/mt
106 Carbon Steel Rebar Rebar HRB400 12mm FOB China USD/mt
107 Carbon Steel Rebar Rebar HRB400 20mm FOB China USD/mt
108 Carbon Steel Rebar Rebar HRB400 20mm In warehouse Guangzhou RMB/mt
109 Carbon Steel Rebar Rebar HRB400 20mm In warehouse Shanghai RMB/mt
110 Carbon Steel Rebar Rebar HRB400 20mm In warehouse Tianjin RMB/mt
111 Carbon Steel Rebar TMT Bar Fe500 12mm In warehouse Mumbai INR/mt
112 Carbon Steel Rebar TMT Bar Fe500 16mm In warehouse Mumbai INR/mt
113 Carbon Steel Sections Angle Steel Q235 50*50*5mm In warehouse Guangzhou RMB/mt
114 Carbon Steel Sections Angle Steel Q235 50*50*5mm In warehouse Shanghai RMB/mt
115 Carbon Steel Sections Angle Steel Q235 50*50*5mm In warehouse Tianjin RMB/mt
116 Carbon Steel Sections Channel Steel Q235 160*65*8.5mm In warehouse Guangzhou RMB/mt
117 Carbon Steel Sections Channel Steel Q235 160*65*8.5mm In warehouse Shanghai RMB/mt
118 Carbon Steel Sections Channel Steel Q235 160*65*8.5mm In warehouse Tianjin RMB/mt
119 Carbon Steel Sections Dysprosium Metal 99.5%min EXW China RMB/kg
120 Carbon Steel Sections Dysprosium Metal 99.5%min FOB China USD/kg
121 Carbon Steel Sections Dysprosium Oxide 99.5%min EXW China RMB/kg
122 Carbon Steel Sections Dysprosium Oxide 99.5%min FOB China USD/kg
123 Carbon Steel Sections Erbium Oxide 99.5%min EXW China RMB/kg
124 Carbon Steel Sections Erbium Oxide 99.5%min FOB China USD/kg
125 Carbon Steel Sections Europium Metal 99.5%min FOB China USD/kg
126 Carbon Steel Sections Europium Oxide 99.999%min EXW China RMB/kg
127 Carbon Steel Sections Europium Oxide 99.999%min FOB China USD/kg
128 Carbon Steel Sections Ferro-dysprosium 80% EXW China RMB/kg
129 Carbon Steel Sections Ferro-dysprosium 80% FOB China USD/kg
130 Carbon Steel Sections H-beam Steel Q235 200*200mm In warehouse Guangzhou RMB/mt
131 Carbon Steel Sections H-beam Steel Q235 200*200mm In warehouse Shanghai RMB/mt
132 Carbon Steel Sections H-beam Steel Q235 200*200mm In warehouse Tianjin RMB/mt
133 Carbon Steel Sections H-beam Steel Q235 300*300mm In warehouse Guangzhou RMB/mt
134 Carbon Steel Sections H-beam Steel Q235 300*300mm In warehouse Shanghai RMB/mt
135 Carbon Steel Sections H-beam Steel Q235 300*300mm In warehouse Tianjin RMB/mt
136 Carbon Steel Sections I-beam Steel Q235 250*118*10mm In warehouse Guangzhou RMB/mt
137 Carbon Steel Sections I-beam Steel Q235 250*118*10mm In warehouse Shanghai RMB/mt
138 Carbon Steel Sections I-beam Steel Q235 250*118*10mm In warehouse Tianjin RMB/mt
139 Carbon Steel Strip HR Strip Q195 295*2.5mm In warehouse Guangzhou RMB/mt
140 Carbon Steel Strip HR Strip Q195 295*2.5mm In warehouse Shanghai RMB/mt
141 Carbon Steel Strip HR Strip Q195 295*2.5mm In warehouse Tianjin RMB/mt
142 Carbon Steel Strip HR Strip Q195 685*2.75mm In warehouse Guangzhou RMB/mt
143 Carbon Steel Strip HR Strip Q235B 685*5.5mm In warehouse Shanghai RMB/mt
144 Carbon Steel Strip HR Strip Q235B 685*5.5mm In warehouse Tianjin RMB/mt
145 Carbon Steel Wire Rod LaCe Mischmetal Ce/RE 65%min, Fe 0.3%max EXW China RMB/mt
146 Carbon Steel Wire Rod LaCe Mischmetal Ce/RE 65%min, Fe 1%max EXW China RMB/mt
147 Carbon Steel Wire Rod LaCe Mischmetal Ce/RE 65%min, Fe 1%max FOB China USD/kg
148 Carbon Steel Wire Rod Wire Rod JIS SWRM15 ?6.5mm In warehouse Bangkok THB/mt
149 Carbon Steel Wire Rod Wire Rod Q195 6.5mm FOB China USD/mt
150 Carbon Steel Wire Rod Wire Rod Q235 8mm In warehouse Guangzhou RMB/mt
151 Carbon Steel Wire Rod Wire Rod Q235 8mm In warehouse Shanghai RMB/mt
152 Carbon Steel Wire Rod Wire Rod Q235 8mm In warehouse Tianjin RMB/mt
153 Carbon Steel Wire Rod Wire Rod SAE1008 5.5mm In warehouse Kuala Lumpur MYR/mt
154 Carbon Steel Wire Rod Wire Rod SAE1008 5.5mm In warehouse Mumbai INR/mt
155 Carbon Steel Wire Rod Wire Rod SAE1008 6.5mm FOB China USD/mt
156 Ferroalloys Calcium Silicon Calcium-silicon 28-55 EXW China RMB/mt
157 Ferroalloys Calcium Silicon Calcium-silicon 28-55 FOB China USD/mt
158 Ferroalloys Calcium Silicon Calcium-silicon 30-60 EXW China RMB/mt
159 Ferroalloys Calcium Silicon Calcium-silicon 30-60 FOB China USD/mt
160 Ferroalloys Chrome Ore Chrome Conc. 40%min FOB S.A. USD/mt
161 Ferroalloys Chrome Ore Chrome Conc. 42%min FOB S.A. USD/mt
162 Ferroalloys Chrome Ore Chrome Conc. 46%min FOB Turkey USD/mt
163 Ferroalloys Chrome Ore Chrome Conc. S.A. 40%min CIF China USD/mt
164 Ferroalloys Chrome Ore Chrome Conc. S.A. 40%min In port China RMB/dmtu
165 Ferroalloys Chrome Ore Chrome Conc. S.A. 42%min CIF China USD/mt
166 Ferroalloys Chrome Ore Chrome Conc. S.A. 42%min In port China RMB/dmtu
167 Ferroalloys Chrome Ore Chrome Conc. Turkish 46%min CIF China USD/mt
168 Ferroalloys Chrome Ore Chrome Conc. Turkish 46%min In port China RMB/dmtu
169 Ferroalloys Chrome Ore Chrome Conc. Zimbabwean 48%min CIF China USD/mt
170 Ferroalloys Chrome Ore Chrome Conc. Zimbabwean 48%min In port China RMB/dmtu
171 Ferroalloys Chrome Ore Chrome Lump 40%min FOB Turkey USD/mt
172 Ferroalloys Chrome Ore Chrome Lump S.A. 38%min CIF China USD/mt
173 Ferroalloys Chrome Ore Chrome Lump S.A. 38%min In port China RMB/dmtu
174 Ferroalloys Chrome Ore Chrome Lump Turkish 40%min CIF China USD/mt
175 Ferroalloys Chrome Ore Chrome Lump Turkish 40%min In port China RMB/dmtu
176 Ferroalloys Chromium Silicon Silico-chrome Si 42%min, Cr 30%min EXW China RMB/mt
177 Ferroalloys Ferroboron Ferro-boron B 18%min EXW China RMB/mt
178 Ferroalloys Ferroboron Ferro-boron B 18%min FOB China USD/mt
179 Ferroalloys Ferroboron Ferro-boron B 19%min EXW China RMB/mt
180 Ferroalloys Ferrochrome Ferro-chrome Cr 52%min, C 8%max EXW China RMB/mt 50-basis
181 Ferroalloys Ferrochrome Ferro-chrome Cr 55%min, C 0.1%max EXW China RMB/mt 60-basis
182 Ferroalloys Ferrochrome Ferro-chrome Cr 55%min, C 0.25%max EXW China RMB/mt 60-basis
183 Ferroalloys Ferrochrome Ferro-chrome Cr 60%min, C 0.1%max FOB China USD/lb Cr
184 Ferroalloys Ferrochrome Ferro-chrome Cr 60%min, C 0.1%max In warehouse Pittsburgh USD/lb Cr
185 Ferroalloys Ferrochrome Ferro-chrome Cr 60%min, C 0.1%max In warehouse Rotterdam USD/lb Cr
186 Ferroalloys Ferrochrome Ferro-chrome Cr 60%min, C 8%max In warehouse Pittsburg USD/lb Cr
187 Ferroalloys Ferrochrome Ferro-chrome Cr 60%min, C 8%max In warehouse Rotterdam USD/lb Cr
188 Ferroalloys Ferrochrome Ferro-chrome Cr 60%min, C 8.5%max In warehouse Russia RUB/kg
189 Ferroalloys Ferrochrome Ferro-chrome Cr 65%min, C 0.1%max In warehouse Russia RUB/kg
190 Ferroalloys Ferrochrome Ferro-chrome Indian Cr 58%min, C 8%max CIF China USD/lb Cr
191 Ferroalloys Ferrochrome Ferro-chrome Indian Cr 58%min, C 8%max In port China RMB/mt 50-basis
192 Ferroalloys Ferrochrome Ferro-chrome Kazakhstan Cr 68%min, C 8.5%max CIF China USD/lb Cr
193 Ferroalloys Ferrochrome Ferro-chrome Kazakhstan Cr 68%min, C 8.5%max In port China RMB/mt 50-basis
194 Ferroalloys Ferrochrome Ferro-chrome S.A. Cr 50%min, C 8%max CIF China USD/lb Cr
195 Ferroalloys Ferrochrome Ferro-chrome S.A. Cr 50%min, C 8%max In port China RMB/mt 50-basis
196 Ferroalloys Ferromanganese Ferro-manganese Mn 65%min, C 7%max EXW China RMB/mt
197 Ferroalloys Ferromanganese Ferro-manganese Mn 75%min, C 2%max EXW China RMB/mt
198 Ferroalloys Ferromanganese Ferro-manganese Mn 75%min, C 8%max EXW China RMB/mt
199 Ferroalloys Ferromanganese Ferro-manganese Mn 78%min, C 1.5%max EXW China RMB/mt
200 Ferroalloys Ferromanganese Ferro-manganese Mn 78%min, C 7%max In warehouse Russia RUB/kg
201 Ferroalloys Ferromanganese Ferro-manganese Mn 78%min, C 8%max Delivered Europe EUR/mt
202 Ferroalloys Ferromanganese Ferro-manganese Mn 78%min, C 8%max In warehouse Pittsburgh USD/gt
203 Ferroalloys Ferromanganese Ferro-manganese Mn 80%min, C 0.7%max EXW China RMB/mt
204 Ferroalloys Ferromolybdenum Ferro-molybdenum 60%min Delivered India INR/kg
205 Ferroalloys Ferromolybdenum Ferro-molybdenum 60%min EXW China RMB/mt
206 Ferroalloys Ferromolybdenum Ferro-molybdenum 60%min In port South Korea USD/kg Mo
207 Ferroalloys Ferromolybdenum Ferro-molybdenum 60%min In warehouse Russia RUB/kg
208 Ferroalloys Ferromolybdenum Ferro-molybdenum 65%min In warehouse Rotterdam USD/kg Mo
209 Ferroalloys Ferromolybdenum Ferro-molybdenum 65%min In warehouse US USD/kg Mo
210 Ferroalloys Ferromolybdenum Molybdenum Oxide 57%min In port South Korea USD/lb
211 Ferroalloys Ferromolybdenum Molybdenum Oxide 57%min In warehouse Rotterdam USD/lb
212 Ferroalloys Ferronickel Ferro-nickel 1.5%min EXW China RMB/mt
213 Ferroalloys Ferronickel Ferro-nickel 10%min EXW China RMB/mtu
214 Ferroalloys Ferronickel Nickel Ore Philippine 1.5%min CIF China USD/mt
215 Ferroalloys Ferronickel Nickel Ore Philippine 1.8%min In port China RMB/mt
216 Ferroalloys Ferroniobium Ferro-niobium 50-A EXW China RMB/mt
217 Ferroalloys Ferroniobium Ferro-niobium 60-A EXW China RMB/mt
218 Ferroalloys Ferroniobium Ferro-niobium Brazilian 66% Delivered China RMB/mt
219 Ferroalloys Ferroniobium Ferro-niobium Brazilian 66% In warehouse Rotterdam USD/kg Nb
220 Ferroalloys Ferroniobium Niobium Conc. Nb2O5 50%min, Ta2O5 5%min, CIF China USD/lb Oxides
221 Ferroalloys Ferroniobium Niobium Pentoxide 99.5%min EXW China RMB/kg
222 Ferroalloys Ferrophosphorus Ferro-phosphorus 23%min FOB China USD/mt
223 Ferroalloys Ferrosilicon Ferro-silicon 65%min FOB Russia USD/mt
224 Ferroalloys Ferrosilicon Ferro-silicon 65%min In warehouse Russia RUB/kg
225 Ferroalloys Ferrosilicon Ferro-silicon 70%min EXW India INR/mt
226 Ferroalloys Ferrosilicon Ferro-silicon 72%min EXW China RMB/mt
227 Ferroalloys Ferrosilicon Ferro-silicon 72%min Ex-VAT FOB China USD/mt
228 Ferroalloys Ferrosilicon Ferro-silicon 72%min FOB China USD/mt
229 Ferroalloys Ferrosilicon Ferro-silicon 75%min EXW China RMB/mt
230 Ferroalloys Ferrosilicon Ferro-silicon 75%min Ex-VAT FOB China USD/mt
231 Ferroalloys Ferrosilicon Ferro-silicon 75%min FOB China USD/mt
232 Ferroalloys Ferrosilicon Ferro-silicon 75%min In warehouse Pittsburgh USD/lb Si
233 Ferroalloys Ferrosilicon Ferro-silicon 75%min In warehouse Rotterdam EUR/mt
234 Ferroalloys Ferrotitanium Ferro-titanium 30% EXW China RMB/mt 25-basis
235 Ferroalloys Ferrotitanium Ferro-titanium 30% EXW India INR/kg
236 Ferroalloys Ferrotitanium Ferro-titanium 70% (scrap) EXW China RMB/mt
237 Ferroalloys Ferrotitanium Ferro-titanium 70% In port India INR/kg
238 Ferroalloys Ferrotitanium Ferro-titanium 70%min Delivered US USD/kg Ti
239 Ferroalloys Ferrotitanium Ferro-titanium 70%min EXW Russia RUB/kg
240 Ferroalloys Ferrotitanium Ferro-titanium 70%min FOB Russia USD/kg Ti
241 Ferroalloys Ferrotitanium Ferro-titanium 70%min In warehouse Rotterdam USD/kg Ti
242 Ferroalloys Ferrotungsten Ferro-tungsten 70%min EXW China RMB/mt
243 Ferroalloys Ferrotungsten Ferro-tungsten 75%min EXW China RMB/mt
244 Ferroalloys Ferrotungsten Ferro-tungsten 75%min FOB China USD/kg W
245 Ferroalloys Ferrotungsten Ferro-tungsten 75%min FOB Russia USD/kg W
246 Ferroalloys Ferrotungsten Ferro-tungsten 75%min In warehouse Rotterdam USD/kg W
247 Ferroalloys Ferrovanadium Ferro-vanadium 50%min CIF China USD/kg V
248 Ferroalloys Ferrovanadium Ferro-vanadium 50%min Delivered China RMB/mt
249 Ferroalloys Ferrovanadium Ferro-vanadium 50%min In port China RMB/mt
250 Ferroalloys Ferrovanadium Ferro-vanadium 50%min In warehouse Russia RUB/kg
251 Ferroalloys Ferrovanadium Ferro-vanadium 80%min FOB China USD/kg V
252 Ferroalloys Ferrovanadium Ferro-vanadium 80%min In warehouse Pittsburgh USD/kg V
253 Ferroalloys Ferrovanadium Ferro-vanadium 80%min In warehouse Rotterdam USD/kg V
254 Ferroalloys Ferrovanadium Vanadium Nitride N 14%min EXW China RMB/mt
255 Ferroalloys Ferrovanadium Vanadium Pentoxide Flake 98%min In warehouse Rotterdam USD/lb V2O5
256 Ferroalloys Manganese Ore Manganese Ore 38%min FOB S.A. USD/dmtu
257 Ferroalloys Manganese Ore Manganese Ore Australian 45%min In Qinzhou Port RMB/mtu
258 Ferroalloys Manganese Ore Manganese Ore Australian 45%min In Tianjin Port RMB/mtu
259 Ferroalloys Manganese Ore Manganese Ore Brazilian 44%min In Qinzhou Port RMB/mtu
260 Ferroalloys Manganese Ore Manganese Ore Brazilian 44%min In Tianjin Port RMB/mtu
261 Ferroalloys Manganese Ore Manganese Ore Carbonate, Mn 13%min EXW China RMB/mtu
262 Ferroalloys Manganese Ore Manganese Ore Gabonese 44%min In Qinzhou Port RMB/mtu
263 Ferroalloys Manganese Ore Manganese Ore Gabonese 44%min In Tianjin Port RMB/mtu
264 Ferroalloys Manganese Ore Manganese Ore S.A. 36%min In Qinzhou Port RMB/mtu
265 Ferroalloys Manganese Ore Manganese Ore S.A. 36%min In Tianjin Port RMB/mtu
266 Ferroalloys Manganese Ore Manganese Ore S.A. 37%min In Qinzhou Port RMB/mtu
267 Ferroalloys Manganese Ore Manganese Ore S.A. 37%min In Tianjin Port RMB/mtu
268 Ferroalloys Silicomanganese Silico-manganese 60/14 EXW China RMB/mt
269 Ferroalloys Silicomanganese Silico-manganese 60/14 FOB India USD/mt
270 Ferroalloys Silicomanganese Silico-manganese 65/17 Delivered Europe EUR/mt
271 Ferroalloys Silicomanganese Silico-manganese 65/17 EXW China RMB/mt
272 Ferroalloys Silicomanganese Silico-manganese 65/17 FOB India USD/mt
273 Ferroalloys Silicomanganese Silico-manganese 65/17 In warehouse Pittsburgh USD/lb
274 Ferroalloys Silicomanganese Silico-manganese 65/17 In warehouse Russia RUB/kg
275 Minor Metals Antimony Antimony Conc. 50%min Delivered China RMB/mt Sb
276 Minor Metals Antimony Antimony Ingot 99.65%min EXW China RMB/mt
277 Minor Metals Antimony Antimony Ingot 99.65%min In port India INR/kg
278 Minor Metals Antimony Antimony Ingot 99.65%min In warehouse Baltimore USD/lb
279 Minor Metals Antimony Antimony Ingot 99.65%min In warehouse Rotterdam USD/mt
280 Minor Metals Antimony Antimony Ingot 99.85%min EXW China RMB/mt
281 Minor Metals Antimony Antimony Trioxide 99.5%min Delivered China RMB/mt
282 Minor Metals Antimony Antimony Trioxide 99.5%min FOB China USD/mt
283 Minor Metals Antimony Antimony Trioxide 99.5%min In warehouse Baltimore USD/lb
284 Minor Metals Antimony Antimony Trioxide 99.5%min In warehouse Rotterdam EUR/kg
285 Minor Metals Antimony Antimony Trioxide 99.8%min Delivered China RMB/mt
286 Minor Metals Antimony Ethylene Glycol Antimony 57%min Delivered China RMB/mt
287 Minor Metals Arsenic Arsenic Metal 99%min EXW China RMB/mt
288 Minor Metals Arsenic Arsenic Metal 99%min FOB China USD/mt
289 Minor Metals Arsenic Arsenic Trioxide 99%min EXW China RMB/mt
290 Minor Metals Arsenic Arsenic Trioxide 99%min FOB China USD/mt
291 Minor Metals Bismuth Bismuth Ingot 99.99%min Delivered Europe USD/lb
292 Minor Metals Bismuth Bismuth Ingot 99.99%min Delivered India INR/kg
293 Minor Metals Bismuth Bismuth Ingot 99.99%min Delivered US USD/lb
294 Minor Metals Bismuth Bismuth Ingot 99.99%min EXW China RMB/mt
295 Minor Metals Bismuth Bismuth Ingot 99.99%min FOB China USD/lb
296 Minor Metals Bismuth Bismuth Oxide 99.9%min EXW China RMB/mt
297 Minor Metals Cadmium Cadmium Ingot 99.99%min Delivered India INR/kg
298 Minor Metals Cadmium Cadmium Ingot 99.99%min EXW China RMB/mt
299 Minor Metals Cadmium Cadmium Ingot 99.99%min In warehouse Baltimore USD/lb
300 Minor Metals Cadmium Cadmium Ingot 99.99%min In warehouse Rotterdam USD/lb
301 Minor Metals Calcium Calcium Granule Electrolytic 98.5%min EXW China RMB/mt
302 Minor Metals Calcium Calcium Granule Electrolytic 98.5%min FOB China USD/mt
303 Minor Metals Calcium Calcium Granule Reductive 98%min EXW China RMB/mt
304 Minor Metals Calcium Calcium Granule Reductive 98%min FOB China USD/mt
305 Minor Metals Calcium Calcium Ingot Reductive 98%min EXW China RMB/mt
306 Minor Metals Calcium Fluorspar 97%min Delivered China RMB/mt
307 Minor Metals Chromium Chromic Anhydride 99.8%min EXW China RMB/mt
308 Minor Metals Chromium Chromium Metal 99%min EXW China RMB/mt
309 Minor Metals Chromium Chromium Metal 99%min FOB China USD/mt
310 Minor Metals Chromium Chromium Oxide Green 99%min EXW China RMB/mt
311 Minor Metals Cobalt Cobalt Carbonate 46%min Delivered China RMB/mt
312 Minor Metals Cobalt Cobalt Chloride 24%min Delivered China RMB/mt
313 Minor Metals Cobalt Cobalt Conc. 6-8% CIF China USD/lb Co
314 Minor Metals Cobalt Cobalt Intermediate 20-30% CIF China USD/lb Co
315 Minor Metals Cobalt Cobalt Metal 99.8%min Delivered China RMB/kg
316 Minor Metals Cobalt Cobalt Metal 99.8%min In warehouse Baltimore USD/lb
317 Minor Metals Cobalt Cobalt Metal 99.8%min In warehouse Rotterdam USD/lb
318 Minor Metals Cobalt Cobalt Monoxide 78%min Delivered China RMB/kg
319 Minor Metals Cobalt Cobalt Oxide 72%min Delivered China RMB/kg
320 Minor Metals Cobalt Cobalt Oxide 72%min In warehouse Rotterdam EUR/kg
321 Minor Metals Cobalt Cobalt Powder 99.5%min Delivered China RMB/kg
322 Minor Metals Cobalt Cobalt Sulfate 20.5%min Delivered China RMB/mt
323 Minor Metals Cobalt Cobalt Tetroxide 73.5%min Delivered China RMB/kg
324 Minor Metals Cobalt Lithium Cobaltate Co 60%min Delivered China RMB/kg
325 Minor Metals Cobalt NCM 523 Delivered China RMB/mt
326 Minor Metals Cobalt NCM 622 Delivered China RMB/mt
327 Minor Metals Cobalt Samarium Cobalt 30H EXW China RMB/kg
328 Minor Metals Gallium Gallium Metal 99.99%min EXW China RMB/kg
329 Minor Metals Gallium Gallium Metal 99.99%min FOB China USD/kg
330 Minor Metals Gallium Gallium Metal 99.99999%min EXW China RMB/kg
331 Minor Metals Germanium Germanium Dioxide 99.99%min Delivered US USD/kg
332 Minor Metals Germanium Germanium Dioxide 99.99%min EXW China RMB/kg
333 Minor Metals Germanium Germanium Dioxide 99.99%min FOB China USD/kg
334 Minor Metals Germanium Germanium Metal 99.99%min Delivered Europe USD/kg
335 Minor Metals Germanium Germanium Metal 99.99%min Delivered US USD/kg
336 Minor Metals Germanium Germanium Metal 99.99%min EXW China RMB/kg
337 Minor Metals Germanium Germanium Metal 99.99%min FOB China USD/kg
338 Minor Metals Indium Indium Ingot 99.995%min Delivered Europe USD/kg
339 Minor Metals Indium Indium Ingot 99.995%min Delivered US USD/kg
340 Minor Metals Indium Indium Ingot 99.995%min EXW China RMB/kg
341 Minor Metals Indium Indium Ingot 99.995%min FOB China USD/kg
342 Minor Metals Lithium LNCMO 523 Delivered China RMB/kg
343 Minor Metals Lithium LNCMO 622 Delivered China RMB/kg
344 Minor Metals Lithium Lithium Carbonate 99%min Delivered China RMB/mt
345 Minor Metals Lithium Lithium Carbonate 99.5%min CIF China USD/kg
346 Minor Metals Lithium Lithium Carbonate 99.5%min Delivered China RMB/mt
347 Minor Metals Lithium Lithium Carbonate 99.5%min Delivered EU USD/kg
348 Minor Metals Lithium Lithium Carbonate 99.5%min Delivered US USD/kg
349 Minor Metals Lithium Lithium Carbonate 99.5%min FOB South America USD/kg
350 Minor Metals Lithium Lithium Chloride 99.3%min Delivered China RMB/mt
351 Minor Metals Lithium Lithium Hydroxide Monohydrate LiOH 56.5%min Delivered China RMB/mt
352 Minor Metals Lithium Lithium Hydroxide Monohydrate LiOH 56.5%min, Magnets 0.0001%max Delivered China RMB/mt
353 Minor Metals Lithium Lithium Hydroxide Monohydrate LiOH 56.5%min, Magnets 0.0001%max Delivered South Korea USD/kg
354 Minor Metals Lithium Lithium Hydroxide Monohydrate LiOH 56.5%min, Magnets 0.0001%max FOB China USD/kg
355 Minor Metals Lithium Lithium Iron Phosphate Li 3.9%min Delivered China RMB/mt
356 Minor Metals Lithium Lithium Manganate Mn 58% Delivered China RMB/mt
357 Minor Metals Lithium Lithium Metal 99%min Delivered China RMB/mt
358 Minor Metals Lithium Lithium Metal 99.9%min Delivered China RMB/mt
359 Minor Metals Lithium Spodumene Li2O 6%min CIF China USD/mt
360 Minor Metals Magnesium Magnesium Alloy AM50A EXW China RMB/mt
361 Minor Metals Magnesium Magnesium Alloy AM50A FOB China USD/mt
362 Minor Metals Magnesium Magnesium Alloy AM60B EXW China RMB/mt
363 Minor Metals Magnesium Magnesium Alloy AM60B FOB China USD/mt
364 Minor Metals Magnesium Magnesium Alloy AZ91D EXW China RMB/mt
365 Minor Metals Magnesium Magnesium Alloy AZ91D FOB China USD/mt
366 Minor Metals Magnesium Magnesium Ingot 99.9%min EXW China RMB/mt
367 Minor Metals Magnesium Magnesium Ingot 99.9%min Ex-VAT FOB China USD/mt
368 Minor Metals Magnesium Magnesium Ingot 99.9%min FOB China USD/mt
369 Minor Metals Magnesium Magnesium Ingot 99.9%min In port India INR/kg
370 Minor Metals Magnesium Magnesium Ingot 99.9%min In warehouse Rotterdam USD/mt
371 Minor Metals Magnesium Magnesium Ingot 99.9%min in warehouse Russia RUB/kg
372 Minor Metals Magnesium Magnesium Ingot 99.95%min EXW China RMB/mt
373 Minor Metals Magnesium Magnesium Powder 99.9%min EXW China RMB/mt
374 Minor Metals Magnesium Magnesium Powder 99.9%min FOB China USD/mt
375 Minor Metals Manganese Manganese Briquette 97%min FOB China USD/mt
376 Minor Metals Manganese Manganese Dioxide Alkaline 91%min EXW China RMB/mt
377 Minor Metals Manganese Manganese Dioxide Carbon 91%min EXW China RMB/mt
378 Minor Metals Manganese Manganese Flake 99.7%min CIF India USD/mt
379 Minor Metals Manganese Manganese Flake 99.7%min EXW China RMB/mt
380 Minor Metals Manganese Manganese Flake 99.7%min Ex-VAT FOB China USD/mt
381 Minor Metals Manganese Manganese Flake 99.7%min FOB China USD/mt
382 Minor Metals Manganese Manganese Flake 99.7%min In warehouse Rotterdam USD/mt
383 Minor Metals Manganese Manganese Lump 96.5%min FOB China USD/mt
384 Minor Metals Manganese Manganese Lump 96.5%min In warehouse Russia RUB/kg
385 Minor Metals Manganese Manganese Sulfate Mn 32%min EXW China RMB/t
386 Minor Metals Mercury Mercury Metal 99.9%min Ex-VAT EXW China RMB/kg
387 Minor Metals Mercury Mercury Metal 99.999%min Ex-VAT EXW China RMB/kg
388 Minor Metals Molybdenum Ammonium Heptamolybdate 54%min EXW China RMB/mt
389 Minor Metals Molybdenum Ammonium Tetrathiomolybdate 56%min EXW China RMB/mt
390 Minor Metals Molybdenum Molybdenum Bar 99.9%min EXW China RMB/kg
391 Minor Metals Molybdenum Molybdenum Conc. 45%min EXW China RMB/mtu
392 Minor Metals Molybdenum Molybdenum Oxide 57%min In port India USD/lb
393 Minor Metals Molybdenum Molybdenum Oxide 50%min EXW China RMB/mtu
394 Minor Metals Molybdenum Molybdenum Oxide 57%min CIF China USD/lb Mo
395 Minor Metals Molybdenum Molybdenum Oxide 57%min In port South Korea USD/lb
396 Minor Metals Molybdenum Molybdenum Oxide 57%min In warehouse Rotterdam USD/lb
397 Minor Metals Molybdenum Molybdenum Oxide 99.9%min EXW China RMB/mt
398 Minor Metals Molybdenum Molybdenum Powder 99.95%min EXW China RMB/kg
399 Minor Metals Molybdenum Molybdenum Scrap Cutting Scrap 99.95%min Ex-VAT EXW China RMB/kg
400 Minor Metals Molybdenum Molybdenum Scrap Wire Scrap 99.95%min Ex-VAT EXW China RMB/kg
401 Minor Metals Molybdenum Sodium Molybdate 99%min EXW China RMB/mt
402 Minor Metals Niobium Niobium Conc. Nb2O5 50%min, Ta2O5 5%min, CIF China USD/lb Oxides
403 Minor Metals Niobium Niobium Pentoxide 99.5%min EXW China RMB/kg
404 Minor Metals Niobium Niobium Pentoxide 99.5%min FOB China USD/kg
405 Minor Metals Niobium Niobium Pentoxide 99.99%min EXW China RMB/kg
406 Minor Metals Niobium Niobium Pentoxide 99.99%min FOB China USD/kg
407 Minor Metals Rhenium Rhenium APR 99.99%min EXW China RMB/kg
408 Minor Metals Selenium Selenium Dioxide 98%min Delivered China RMB/kg
409 Minor Metals Selenium Selenium Powder 99.5%min In warehouse Rotterdam USD/lb
410 Minor Metals Selenium Selenium Powder 99.9%min CIF China USD/lb
411 Minor Metals Selenium Selenium Powder 99.9%min Delivered China RMB/kg
412 Minor Metals Selenium Selenium Powder 99.9%min Delivered India INR/kg
413 Minor Metals Selenium Selenium Powder 99.9%min Delivered US USD/lb
414 Minor Metals Selenium Selenium Powder 99.9%min FOB Russia USD/kg
415 Minor Metals Selenium Selenium Powder 99.95%min Delivered China RMB/kg
416 Minor Metals Silicon Polysilicon 9N Delivered China RMB/kg
417 Minor Metals Silicon Silicon Metal 2-2-02 Delivered China RMB/mt
418 Minor Metals Silicon Silicon Metal 2-2-02 Ex-VAT FOB China USD/mt
419 Minor Metals Silicon Silicon Metal 2-2-02 FOB China USD/mt
420 Minor Metals Silicon Silicon Metal 3-3-03 Delivered China RMB/mt
421 Minor Metals Silicon Silicon Metal 3-3-03 Ex-VAT FOB China USD/mt
422 Minor Metals Silicon Silicon Metal 3-3-03 FOB China USD/mt
423 Minor Metals Silicon Silicon Metal 3-3-03 P 50ppm max Delivered China RMB/mt
424 Minor Metals Silicon Silicon Metal 3-3-03 P 50ppm max Ex-VAT FOB China USD/mt
425 Minor Metals Silicon Silicon Metal 3-3-03 P 50ppm max FOB China USD/mt
426 Minor Metals Silicon Silicon Metal 4-2-1 Delivered China RMB/mt
427 Minor Metals Silicon Silicon Metal 4-4-1 Delivered China RMB/mt
428 Minor Metals Silicon Silicon Metal 4-4-1 Delivered Europe EUR/mt
429 Minor Metals Silicon Silicon Metal 4-4-1 Ex-VAT FOB China USD/mt
430 Minor Metals Silicon Silicon Metal 4-4-1 FOB China USD/mt
431 Minor Metals Silicon Silicon Metal 4-4-1 In port India INR/kg
432 Minor Metals Silicon Silicon Metal 5-5-3 Delivered China RMB/mt
433 Minor Metals Silicon Silicon Metal 5-5-3 Delivered Europe EUR/mt
434 Minor Metals Silicon Silicon Metal 5-5-3 Ex-VAT FOB China USD/mt
435 Minor Metals Silicon Silicon Metal 5-5-3 FOB China USD/mt
436 Minor Metals Silicon Silicon Metal 5-5-3 In port India INR/kg
437 Minor Metals Silicon Silicon Metal 98.5%min Delivered US USD/lb
438 Minor Metals Silicon Silicon Powder 99%min Delivered China RMB/mt
439 Minor Metals Strontium Strontium Metal 99%min EXW China RMB/mt
440 Minor Metals Strontium Strontium Metal 99%min FOB China USD/mt
441 Minor Metals Tantalum Tantalum Conc. Ta2O5 30%min CIF China USD/lb Ta2O5
442 Minor Metals Tantalum Tantalum Conc. Ta2O5 30%min in warehouse Rotterdam USD/lb Ta2O5
443 Minor Metals Tantalum Tantalum Metal 99.95%min Delivered US USD/kg
444 Minor Metals Tantalum Tantalum Metal 99.95%min EXW China RMB/kg
445 Minor Metals Tantalum Tantalum Metal 99.95%min FOB China USD/kg
446 Minor Metals Tantalum Tantalum Pentoxide 99.5%min EXW China RMB/kg
447 Minor Metals Tantalum Tantalum Pentoxide 99.5%min FOB China USD/kg
448 Minor Metals Tantalum Tantalum Pentoxide 99.99%min EXW China RMB/kg
449 Minor Metals Tantalum Tantalum Pentoxide 99.99%min FOB China USD/kg
450 Minor Metals Tellurium Tellurium Metal 99.9%min In warehouse Rotterdam USD/kg
451 Minor Metals Tellurium Tellurium Metal 99.99%min EXW China RMB/kg
452 Minor Metals Tellurium Tellurium Metal 99.99%min In warehouse Rotterdam USD/kg
453 Minor Metals Titanium Ilmenite Conc Australian TiO2 55-58%min, Fe 27%min CIF China USD/mt
454 Minor Metals Titanium Ilmenite Conc Kenyan TiO2 48%min, Fe2O3 20%max CIF China USD/mt
455 Minor Metals Titanium Ilmenite Conc Mozambican TiO2 52%min, Fe2O3 25%max CIF China USD/mt
456 Minor Metals Titanium Ilmenite Conc TiO2 46%min; Fe2O3 8%max; Ex-VAT EXW China RMB/mt
457 Minor Metals Titanium Ilmenite Conc TiO2 50%min; Fe 30%min; In port China RMB/mt
458 Minor Metals Titanium Ilmenite Conc TiO2 50%min; Fe2O3 14%max; In port China RMB/mt
459 Minor Metals Titanium Ilmenite Conc TiO2 52%min, Fe2O3 25%max FOB Vietnam USD/mt
460 Minor Metals Titanium Reduced Ilmenite TiO2 56%min EXW China RMB/mt
461 Minor Metals Titanium Rutile Conc TiO2 95%min In port China RMB/mt
462 Minor Metals Titanium Titanium Dioxide A TiO2 98%min Delivered China RMB/mt
463 Minor Metals Titanium Titanium Dioxide R TiO2 93%min Delivered China RMB/mt
464 Minor Metals Titanium Titanium Dioxide R TiO2 93%min FOB China USD/mt
465 Minor Metals Titanium Titanium Pipe TA2 25.4x1mm EXW China RMB/kg
466 Minor Metals Titanium Titanium Plate TA2 2mm EXW China RMB/kg
467 Minor Metals Titanium Titanium Slag TiO2 74%min EXW China RMB/mt
468 Minor Metals Titanium Titanium Slag TiO2 90%min EXW China RMB/mt
469 Minor Metals Titanium Titanium Sponge 99.7%min EXW China RMB/mt
470 Minor Metals Titanium Titanium Tetrachloride 99.99%min EXW China RMB/mt
471 Minor Metals Tungsten Sodium Tungstate WO3 68%min EXW China RMB/mt
472 Minor Metals Tungsten Thorium Tungsten Electrode ThO2 1.7-2.2% EXW China RMB/kg
473 Minor Metals Tungsten Tungsten APT 88.5%min EXW China RMB/mt
474 Minor Metals Tungsten Tungsten APT 88.5%min FOB China USD/mtu
475 Minor Metals Tungsten Tungsten APT 88.5%min In warehouse Rotterdam USD/mtu
476 Minor Metals Tungsten Tungsten Bar W-4 99.9%min EXW China RMB/kg
477 Minor Metals Tungsten Tungsten Bar W-4 99.9%min FOB China USD/kg
478 Minor Metals Tungsten Tungsten Carbide 99.8%min 2.5-7.0?m EXW China RMB/kg
479 Minor Metals Tungsten Tungsten Carbide 99.8%min 2.5-7.0?m FOB China USD/kg
480 Minor Metals Tungsten Tungsten Ore WO3 50%min FOB Africa USD/mtu
481 Minor Metals Tungsten Tungsten Ore WO3 65%min EXW China RMB/mt
482 Minor Metals Tungsten Tungsten Oxide WO3 99.95%min EXW China RMB/mt
483 Minor Metals Tungsten Tungsten Oxide WO3 99.95%min FOB China USD/mtu
484 Minor Metals Tungsten Tungsten Powder 99.95%min 2.5-7.0?m EXW China RMB/kg
485 Minor Metals Tungsten Tungsten Scrap Anvil 90%min Ex-VAT Delivered China RMB/kg
486 Minor Metals Tungsten Tungsten Scrap CNC Cutting Blades 70%min Ex-VAT Delivered China RMB/kg
487 Minor Metals Tungsten Tungsten Scrap Drills 70%min Ex-VAT Delivered China RMB/kg
488 Minor Metals Tungsten Tungsten Scrap Grinding Swarf W 70%min, Co 5%min Ex-VAT Delivered China RMB/mtu
489 Minor Metals Vanadium Ammonium Metavanadate 98%min EXW China RMB/mt
490 Minor Metals Vanadium Vanadium Nitride N 14%min EXW China RMB/mt
491 Minor Metals Vanadium Vanadium Pentoxide Flake 92%min CIF China USD/lb V2O5
492 Minor Metals Vanadium Vanadium Pentoxide Flake 92%min In port China RMB/mt
493 Minor Metals Vanadium Vanadium Pentoxide Flake 98%min CIF China USD/lb V2O5
494 Minor Metals Vanadium Vanadium Pentoxide Flake 98%min EXW China RMB/mt
495 Minor Metals Vanadium Vanadium Pentoxide Flake 98%min In port China RMB/mt
496 Minor Metals Vanadium Vanadium Pentoxide Flake 98%min In warehouse Rotterdam USD/lb V2O5
497 Minor Metals Vanadium Vanadium Pentoxide Powder 98%min EXW China RMB/mt
498 Minor Metals Vanadium Vanadium Pentoxide Powder 99%min EXW China RMB/mt
499 Minor Metals Vanadium Vanadium Pentoxide Powder 99.5%min EXW China RMB/mt
500 Minor Metals Zirconium Fused Zirconia Zr(Hf)O2 98.5%min EXW China RMB/mt
501 Minor Metals Zirconium Zircon Sand Australian Zr(Hf)O2 66%min In port China RMB/mt
502 Minor Metals Zirconium Zircon Sand Australian Zr(Hf)O2 66%min In port China USD/mt
503 Minor Metals Zirconium Zircon Sand Indonesian Zr(Hf)O2 66%min CIF China USD/mt
504 Minor Metals Zirconium Zircon Sand S.A. Zr(Hf)O2 65%min In port China RMB/mt
505 Minor Metals Zirconium Zircon Sand S.A. Zr(Hf)O2 65%min In port China USD/mt
506 Minor Metals Zirconium Zircon Sand S.A. Zr(Hf)O2 66%min In port China USD/mt
507 Minor Metals Zirconium Zircon Sand Zr(Hf)O2 65%min EXW China RMB/mt
508 Minor Metals Zirconium Zirconium Carbonate Zr(Hf)O2 40%min EXW China RMB/mt
509 Minor Metals Zirconium Zirconium Carbonate Zr(Hf)O2 40%min, Cl 50ppm EXW China RMB/mt
510 Minor Metals Zirconium Zirconium Oxychloride Zr(Hf)O2 36%min EXW China RMB/mt
511 Minor Metals Zirconium Zirconium Silicate Zr(Hf)O2 64.5%min EXW China RMB/mt
512 Minor Metals Zirconium Zirconium Sponge Zr+Hf 99.4%min EXW China RMB/kg
513 Rare Earths Lutetium Lutetium Oxide 99.99%min China RMB/kg
514 Rare Earths Neodymium Cerium Carbonate TREO 45%min, CeO2/REO 100% EXW China RMB/mt
515 Rare Earths Neodymium Cerium Carbonate TREO 45%min, CeO2/REO 100% FOB China USD/mt
516 Rare Earths Neodymium Cerium Metal 99%min EXW China RMB/mt
517 Rare Earths Neodymium Cerium Metal 99%min FOB China USD/kg
518 Rare Earths Neodymium Cerium Oxide 99%min FOB China USD/mt
519 Rare Earths Neodymium Cerium Oxide 99.9%min EXW China RMB/mt
520 Rare Earths Neodymium Cerium Oxide 99.9%min In warehouse Rotterdam USD/kg
521 Rare Earths Neodymium Cerium Oxide 99.99%min EXW China RMB/mt
522 Rare Earths Neodymium LaCe Carbonate TREO 45%min, CeO2/REO 65±2% EXW China RMB/mt
523 Rare Earths Neodymium LaCe Chloride TREO 45%min, CeO2/REO 65±2% EXW China RMB/mt
524 Rare Earths Neodymium NdFeB Sintered Rough 35H EXW China RMB/kg
525 Rare Earths Neodymium NdFeB Sintered Rough 35M EXW China RMB/kg
526 Rare Earths Neodymium NdFeB Sintered Rough 45H EXW China RMB/kg
527 Rare Earths Neodymium NdFeB Sintered Rough 45M EXW China RMB/kg
528 Rare Earths Neodymium NdFeB Sintered Rough 48H EXW China RMB/kg
529 Rare Earths Neodymium NdFeB Sintered Rough 50H EXW China RMB/kg
530 Rare Earths Neodymium NdFeB Sintered Rough 50M EXW China RMB/kg
531 Rare Earths Neodymium NdFeB Sintered Rough N35 EXW China RMB/kg
532 Rare Earths Neodymium NdFeB Sintered Rough N45 EXW China RMB/kg
533 Rare Earths Neodymium NdFeB Sintered Rough N52 EXW China RMB/kg
534 Rare Earths Neodymium Neodymium Metal 99%min EXW China RMB/mt
535 Rare Earths Neodymium Neodymium Metal 99%min FOB China USD/kg
536 Rare Earths Neodymium Neodymium Oxide 99.5%min EXW China RMB/mt
537 Rare Earths Neodymium Neodymium Oxide 99.5%min FOB China USD/mt
538 Rare Earths Neodymium PrNd Mischmetal Pr 25%, Nd 75% EXW China RMB/mt
539 Rare Earths Neodymium PrNd Mischmetal Pr 25%, Nd 75% FOB China USD/kg
540 Rare Earths Neodymium PrNd Oxide Pr6O11 25%, Nd2O3 75% EXW China RMB/mt
541 Rare Earths Praseodymium Praseodymium Metal 99.5%min FOB China USD/kg
542 Rare Earths Praseodymium Praseodymium Oxide 99.5%min EXW China RMB/mt
543 Rare Earths Praseodymium Praseodymium Oxide 99.5%min FOB China USD/mt
544 Rare Earths Praseodymium Praseodymium Oxide 99.5%min In warehouse Rotterdam USD/kg
545 Rare Earths Samarium Samarium Cobalt 30H EXW China RMB/kg
546 Rare Earths Samarium Samarium Metal 99.5%min EXW China RMB/kg
547 Rare Earths Samarium Samarium Metal 99.5%min FOB China USD/kg
548 Rare Earths Samarium Samarium Oxide 99.9%min EXW China RMB/mt
549 Rare Earths Samarium Samarium Oxide 99.9%min FOB China USD/mt
550 Rare Earths Samarium SmEuGd Oxide TREO 220g/L min, Eu2O3/REO 8%min EXW China RMB/mt
551 Rare Earths Scandium Scandium Aluminum Sc 2% EXW China RMB/kg
552 Rare Earths Scandium Scandium Metal 99.99%min EXW China RMB/kg
553 Rare Earths Scandium Scandium Metal 99.999%min EXW China RMB/kg
554 Rare Earths Scandium Scandium Oxide 99.99%min EXW China RMB/kg
555 Rare Earths Terbium Terbium Metal 99.9%min EXW China RMB/kg
556 Rare Earths Terbium Terbium Metal 99.9%min FOB China USD/kg
557 Rare Earths Terbium Terbium Oxide 99.99%min EXW China RMB/kg
558 Rare Earths Terbium Terbium Oxide 99.99%min FOB China USD/kg
559 Rare Earths Ytterbium Ytterbium Oxide 99.99%min EXW China RMB/kg
560 Rare Earths Yttrium YEu Oxide Eu2O3/REO 6.6%min EXW China RMB/kg
561 Rare Earths Yttrium YEu Oxide Eu2O3/REO 6.6%min FOB China USD/kg
562 Rare Earths Yttrium Yttrium Metal 99.9%min EXW China RMB/kg
563 Rare Earths Yttrium Yttrium Metal 99.9%min FOB China USD/kg
564 Rare Earths Yttrium Yttrium Oxide 99.999%min EXW China RMB/mt
565 Rare Earths Yttrium Yttrium Oxide 99.999%min FOB China USD/kg
566 Rare Earths Yttrium Yttrium Oxide 99.999%min In warehouse Rotterdam USD/kg
567 Refractories Calcined Bauxite Calcined Bauxite 85%min 1-3mm EXW China RMB/mt
568 Refractories Calcined Bauxite Calcined Bauxite 86%min 1-3mm FOB China USD/mt
569 Refractories Calcined Bauxite Calcined Bauxite 87%min 1-3mm EXW China RMB/mt
570 Refractories Carbon Calcined Pet Coke S 3%max EXW China RMB/mt
571 Refractories Carbon Coal Tar Pitch Coking Value 53%min EXW China RMB/mt
572 Refractories Carbon Graphite Electrode H.P. D300mm FOB China USD/mt
573 Refractories Carbon Graphite Electrode H.P. D400mm EXW China RMB/mt
574 Refractories Carbon Graphite Electrode H.P. D450mm FOB China USD/mt
575 Refractories Carbon Graphite Electrode H.P. D500mm EXW China RMB/mt
576 Refractories Carbon Graphite Electrode R.P. D400mm EXW China RMB/mt
577 Refractories Carbon Graphite Electrode U.H.P. D500mm EXW China RMB/mt
578 Refractories Carbon Graphite Electrode U.H.P. D500mm FOB China USD/mt
579 Refractories Carbon Graphite Electrode U.H.P. D600mm EXW China RMB/mt
580 Refractories Carbon Graphite Electrode U.H.P. D600mm FOB China USD/mt
581 Refractories Carbon Pet Coke S 2%max EXW China RMB/mt
582 Refractories Carbon Pet Coke S 3%max EXW China RMB/mt
583 Refractories Carbon Prebaked Anode S 3%max EXW China RMB/mt
584 Refractories Carbon Prebaked Anode S 3%max FOB China USD/mt
585 Refractories Fused Alumina Calcined Alumina 99%min 1-3mm US USD/st
586 Refractories Fused Alumina Fused Alumina Brown 95%min 1-3mm EXW China RMB/mt
587 Refractories Fused Alumina Fused Alumina Brown 95%min 1-3mm EXW India INR/mt
588 Refractories Fused Alumina Fused Alumina Brown 95%min 1-3mm FOB China USD/mt
589 Refractories Fused Alumina Fused Alumina Brown 95%min 1-3mm In warehouse Rotterdam EUR/mt
590 Refractories Fused Alumina Fused Alumina Brown 95%min 1-3mm US USD/st
591 Refractories Fused Alumina Fused Alumina Brown 95%min F36 EXW China RMB/mt
592 Refractories Fused Alumina Fused Alumina Brown 95%min F36 EXW India INR/mt
593 Refractories Fused Alumina Fused Alumina Brown 95%min F36 FOB China USD/mt
594 Refractories Fused Alumina Fused Alumina White 99%min 1-3mm EXW China RMB/mt
595 Refractories Fused Alumina Fused Alumina White 99%min 1-3mm FOB China USD/mt
596 Refractories Fused Alumina Fused Alumina White 99%min 1-3mm In warehouse Rotterdam EUR/mt
597 Refractories Fused Alumina Fused Alumina White 99%min 1-3mm US USD/st
598 Refractories Fused Alumina Fused Alumina White 99%min F36 EXW China RMB/mt
599 Refractories Fused Alumina Fused Alumina White 99%min F36 FOB China USD/mt
600 Refractories Fused Alumina Tabular Alumina 99%min 1-3mm US USD/st
601 Refractories Graphite Amorphous Graphite 75%min 0-3mm EXW China RMB/mt
602 Refractories Graphite Amorphous Graphite 80%min 20-50mm EXW China RMB/mt
603 Refractories Graphite Amorphous Graphite 80%min 200mesh min EXW China RMB/mt
604 Refractories Graphite Expansible Graphite 99 80 250 EXW China RMB/mt
605 Refractories Graphite Flake Graphite +195 EXW China RMB/mt
606 Refractories Graphite Flake Graphite +895 EXW China RMB/mt
607 Refractories Graphite Flake Graphite -190 EXW China RMB/mt
608 Refractories Graphite Flake Graphite -194 EXW China RMB/mt
609 Refractories Graphite Flake Graphite -194 FOB China USD/mt
610 Refractories Graphite Flake Graphite -199 EXW China RMB/mt
611 Refractories Graphite Spherical Graphite 99.95%min 17?m max EXW China RMB/mt
612 Refractories Magnesia Magnesia C.C. 85%min 200mesh EXW China RMB/mt
613 Refractories Magnesia Magnesia C.C. 90%min 200mesh EXW China RMB/mt
614 Refractories Magnesia Magnesia C.C. 90%min 200mesh FOB China USD/mt
615 Refractories Magnesia Magnesia C.C. 97%min 0-5mm EXW China RMB/mt
616 Refractories Magnesia Magnesia D.B. 90%min 3-15mm EXW China RMB/mt
617 Refractories Magnesia Magnesia D.B. 90%min 3-15mm FOB China USD/mt
618 Refractories Magnesia Magnesia D.B. 90%min 3-15mm In warehouse Rotterdam EUR/mt
619 Refractories Magnesia Magnesia D.B. 90%min 3-15mm US USD/st
620 Refractories Magnesia Magnesia D.B. 95%min 0-30mm EXW China RMB/mt
621 Refractories Magnesia Magnesia D.B. 95%min 0-30mm FOB China USD/mt
622 Refractories Magnesia Magnesia D.B. 95%min 0-30mm In warehouse Rotterdam EUR/mt
623 Refractories Magnesia Magnesia D.B. 96.5%min 0-30mm EXW China RMB/mt
624 Refractories Magnesia Magnesia D.B. 97%min 0-30mm EXW China RMB/mt
625 Refractories Magnesia Magnesia D.B. 97%min 0-30mm FOB China USD/mt
626 Refractories Magnesia Magnesia E.F. 97%min EXW China RMB/mt
627 Refractories Magnesia Magnesia E.F. 97%min H-Ca EXW China RMB/mt
628 Refractories Magnesia Magnesia E.F. 97%min H-Ca FOB China USD/mt
629 Refractories Magnesia Magnesia E.F. 97.5%min Large Crystal EXW China RMB/mt
630 Refractories Magnesia Magnesia E.F. 97.5%min Large Crystal FOB China USD/mt
631 Refractories Magnesia Magnesite Lump 47%min EXW China RMB/mt
632 Refractories Magnesia Magnesite Pellet 47%min EXW China RMB/mt
633 Refractories Silicon Carbide Silicon Carbide Black 88%min 0-10mm EXW China RMB/mt
634 Refractories Silicon Carbide Silicon Carbide Black 88%min 0-10mm FOB China USD/mt
635 Refractories Silicon Carbide Silicon Carbide Black 98%min 10-30cm EXW China RMB/mt
636 Refractories Silicon Carbide Silicon Carbide Black 98%min F16-100 EXW China RMB/mt
637 Refractories Silicon Carbide Silicon Carbide Black 98%min F16-100 FOB China USD/mt
638 Refractories Silicon Carbide Silicon Carbide Green 98%min F240 EXW China RMB/mt
639 Refractories Silicon Carbide Silicon Carbide Green 98%min F240 FOB China USD/mt
640 Refractories Silicon Carbide Silicon Carbide Green 98%min JIS2000 EXW China RMB/mt
641 Refractories Silicon Carbide Silicon Carbide Green 98%min JIS2000 FOB China USD/mt
642 Refractories Silicon Carbide Silicon Carbide Green 98.5%min 10-30cm EXW China RMB/mt
643 Refractories Silicon Carbide Silicon Carbide Black 88%min 0-10mm In warehouse Rotterdam EUR/mt
644 Stainless & Special Bearing Steel Cold Heading Wire 22A ?6.5mm In warehouse Shanghai RMB/mt
645 Stainless & Special Bearing Steel Electrical Steel Grain Oriented 130 0.3*980mm In warehouse China RMB/mt
646 Stainless & Special Bearing Steel Electrical Steel Non-grain 470 0.5*1200mm In warehouse China RMB/mt
647 Stainless & Special Bearing Steel Electrical Steel Non-grain 800 0.5*1,200mm In warehouse China RMB/mt
648 Stainless & Special Bearing Steel Stainless Bar 321 60mm In warehouse Dainan RMB/mt
649 Stainless & Special Bearing Steel Stainless CR Coil 201/2B 1.0mm In warehouse China RMB/mt
650 Stainless & Special Bearing Steel Stainless CR Coil 304/2B 1.0mm In warehouse China RMB/mt
651 Stainless & Special Bearing Steel Stainless CR Coil 430/2B 1.0mm In warehouse China RMB/mt
652 Stainless & Special Bearing Steel Stainless HR Coil 201/No.1 6.0mm In warehouse China RMB/mt
653 Stainless & Special Bearing Steel Stainless HR Coil 304/No.1 6.0mm In warehouse China RMB/mt
654 Stainless & Special Stainless Bar Stainless Bar 304 60mm In warehouse Dainan RMB/mt
655 Stainless & Special Stainless Bar Stainless Bar 321 60mm In warehouse Dainan RMB/mt
656 Stainless & Special Stainless Pipe Stainless Seamless Pipe 304 108*4mm In warehouse Wenzhou RMB/mt
657 Stainless & Special Stainless Scrap Stainless Scrap 304 Solid In warehouse Tokyo JPY/mt
658 Stainless & Special Stainless Scrap Stainless Scrap 304 Solid In warehouse Dainan RMB/mt
659 Stainless & Special Structural Steel Stainless CR Coil 304/2B 1.0mm In warehouse China RMB/mt
660 Steel Raw Materials Coal Coking Coal A 10%max, S 0.8%max EXW Hebei RMB/mt
661 Steel Raw Materials Coal Coking Coal A 10%max, S 0.8%max EXW Henan RMB/mt
662 Steel Raw Materials Coal Coking Coal A 10.5%max, S 0.8%max EXW Shanxi RMB/mt
663 Steel Raw Materials Coal Thermal Coal Q5000 In port Qinhuangdao RMB/mt
664 Steel Raw Materials Coal Thermal Coal Q5500 In port Qinhuangdao RMB/mt
665 Steel Raw Materials Coke Met. Coke A 13%max, S 0.7%max EXW Hebei RMB/mt
666 Steel Raw Materials Coke Met. Coke A 13%max, S 0.7%max EXW Shandong RMB/mt
667 Steel Raw Materials Coke Met. Coke A 13%max, S 0.7%max EXW Shanxi RMB/mt
668 Steel Raw Materials Iron Cast Iron Z18-22 EXW China RMB/mt
669 Steel Raw Materials Iron Pig Iron L8-10 EXW China RMB/mt
670 Steel Raw Materials Iron S.G. Cast Iron Q10-12 EXW China RMB/mt
671 Steel Raw Materials Iron Ore Iron Ore Fine 65%min EXW Benxi RMB/dt
672 Steel Raw Materials Iron Ore Iron Ore Fine 65%min EXW Laiwu RMB/dt
673 Steel Raw Materials Iron Ore Iron Ore Fine 65%min Ex-VAT EXW Daixian RMB/wt
674 Steel Raw Materials Iron Ore Iron Ore Fine 66%min Ex-VAT EXW Handan RMB/dt
675 Steel Raw Materials Iron Ore Iron Ore Fine Australian 58%min In port China RMB/wt
676 Steel Raw Materials Iron Ore Iron Ore Fine Australian 62%min CNF China USD/dt
677 Steel Raw Materials Iron Ore Iron Ore Fine Australian 62%min In port China RMB/wt
678 Steel Raw Materials Iron Ore Iron Ore Fine Brazilian 65%min In port China RMB/wt
679 Steel Raw Materials Iron Ore Iron Ore Lump Australian 62%min In port China RMB/wt
680 Steel Raw Materials Steel Billet Steel Billet HRB335 150x150mm EXW China RMB/mt
681 Steel Raw Materials Steel Billet Steel Billet Q235 150x150mm EXW China RMB/mt
682 Steel Raw Materials Steel Scrap Steel Scrap HMS 4-6mm EXW Shandong RMB/mt
683 Steel Raw Materials Steel Scrap Steel Scrap HMS 8mm min EXW Wuxi RMB/mt
684 Steel Raw Materials Steel Scrap Steel Scrap MMS 4-6mm EXW Guangzhou RMB/mt
685 Steel Raw Materials Steel Scrap Steel Scrap MMS 4-6mm EXW Tianjin RMB/mt
686 Steel Raw Materials Steel Scrap Steel Scrap MMS 6-10mm EXW Liaoning RMB/mt
687 Steel Raw Materials Steel Scrap Steel Scrap Process Scrap Delivered China RMB/mt

920
PRODUCT-CATALOG.json Normal file
View File

@ -0,0 +1,920 @@
{
"unique_product_names": 674,
"catalog_entries": 686,
"groups": {
"Base Metals": {
"Copper": [
"Blister Copper TC 98.5%min Delivered China RMB/mt",
"Copper Cathode 99.99%min Delivered China RMB/mt",
"Copper Conc. 20%min Delivered China RMB/mt Cu",
"Copper Conc. TC 25%min CIF China USD/mt",
"Copper Rod TC ?8mm EXW China RMB/mt",
"Copper Scrap Wire 99%min Delivered China RMB/mt"
],
"Lead": [
"Lead Conc. 60%min EXW China RMB/mt Pb",
"Lead Conc. TC 50%min CIF China USD/mt",
"Lead Ingot 99.994%min In warehouse Guangzhou RMB/mt",
"Lead Ingot 99.994%min In warehouse Shanghai RMB/mt",
"Lead Ingot Secondary 98%min EXW China RMB/mt",
"Lead Ingot Secondary 98%min Ex-VAT EXW China RMB/mt",
"Lead Ingot Secondary 99.994%min EXW China RMB/mt",
"Lead Ingot Secondary 99.994%min Ex-VAT EXW China RMB/mt",
"Lead Scrap Recycled Deep Cycle Battery Ex-VAT Delivered China RMB/mt",
"Lead Scrap Recycled Start-type Battery Ex-VAT Delivered China RMB/mt"
],
"Nickel": [
"Nickel Cathode 99.96%min EXW China RMB/mt",
"Nickel Cathode Norilsk 99.96%min In port China RMB/mt",
"Nickel Ore Philippine 0.8%min CIF China USD/mt",
"Nickel Ore Philippine 0.8%min In port China RMB/mt",
"Nickel Ore Philippine 1.3%min CIF China USD/mt",
"Nickel Ore Philippine 1.5%min CIF China USD/mt",
"Nickel Ore Philippine 1.5%min In port China RMB/mt",
"Nickel Ore Philippine 1.8%min CIF China USD/mt",
"Nickel Sulfate Ni 22%min; Co 0.05%max Delivered China RMB/mt",
"Nickel Sulfate Ni 22%min; Co 0.4%max Delivered China RMB/mt"
],
"Tin": [
"Tin Conc. 60%min Delivered China RMB/mtm",
"Tin Conc. Burmese 20%min In warehouse MengA RMB/mtm",
"Tin Conc. Burmese 30%min In warehouse MengA RMB/mtm",
"Tin Ingot 99.9%min EXW China RMB/mt",
"Tin Solder Wire Sn 99%min, Ag 0.3%min, Cu 0.7%max EXW China RMB/kg",
"Tin Solder Wire Sn 99%min, Cu 0.7%max EXW China RMB/kg"
],
"Zinc": [
"Zinc Conc. 50%min EXW North China RMB/mt Zn",
"Zinc Conc. TC 50% CIF China USD/mt"
]
},
"Carbon Steel": {
"Coated": [
"HGI Coil DX51D 0.5mm FOB China USD/mt",
"HGI Coil DX51D 0.5mm In warehouse Guangzhou RMB/mt",
"HGI Coil DX51D 0.5mm In warehouse Shanghai RMB/mt",
"HGI Coil DX51D 0.5mm In warehouse Tianjin RMB/mt",
"HGI Coil DX51D 1.0mm FOB China USD/mt",
"HGI Coil DX51D 1.0mm In warehouse Guangzhou RMB/mt",
"HGI Coil DX51D 1.0mm In warehouse Shanghai RMB/mt",
"HGI Coil DX51D 1.0mm In warehouse Tianjin RMB/mt",
"HGI Coil IS513 0.5mm In warehouse Mumbai INR/mt"
],
"Cold Rolled Coil": [
"CR Coil DC01 1.0mm In warehouse Istanbul USD/mt",
"CR Coil IS513 1.0mm In warehouse Mumbai INR/mt",
"CR Coil SPCC 0.5mm FOB China USD/mt",
"CR Coil SPCC 0.5mm In warehouse Guangzhou RMB/mt",
"CR Coil SPCC 0.5mm In warehouse Shanghai RMB/mt",
"CR Coil SPCC 0.5mm In warehouse Tianjin RMB/mt",
"CR Coil SPCC 1.0mm FOB China USD/mt",
"CR Coil SPCC 1.0mm In warehouse Guangzhou RMB/mt",
"CR Coil SPCC 1.0mm In warehouse Hanoi VND/mt",
"CR Coil SPCC 1.0mm In warehouse Kuala Lumpur MYR/mt",
"CR Coil SPCC 1.0mm In warehouse Shanghai RMB/mt",
"CR Coil SPCC 1.0mm In warehouse Tianjin RMB/mt",
"LaCe Chloride TREO 45%min, CeO2/REO 65±2% EXW China RMB/mt",
"LaCe Mischmetal Ce/RE 65%min, Fe 0.3%max EXW China RMB/mt",
"Lanthanum Oxide 99.9%min FOB China USD/mt",
"Lanthanum Oxide 99.999%min EXW China RMB/mt",
"Lanthanum Oxide 99.999%min FOB China USD/kg"
],
"Hot Rolled Coil": [
"Ferro-gadolinium Gd 73% EXW China RMB/mt",
"Ferro-holmium 80% EXW China RMB/kg",
"Gadolinium Oxide 99.5%min EXW China RMB/mt",
"Gadolinium Oxide 99.99%min EXW China RMB/mt",
"HR Coil S275 JR 3.0mm In warehouse Istanbul USD/mt",
"HR Coil IS2062 3.0mm In warehouse Mumbai INR/mt",
"HR Coil Q235B 2.75mm In warehouse Guangzhou RMB/mt",
"HR Coil Q235B 2.75mm In warehouse Shanghai RMB/mt",
"HR Coil Q235B 2.75mm In warehouse Tianjin RMB/mt",
"HR Coil Q235B 5.5mm In warehouse Guangzhou RMB/mt",
"HR Coil Q235B 5.5mm In warehouse Shanghai RMB/mt",
"HR Coil Q235B 5.5mm In warehouse Tianjin RMB/mt",
"HR Coil SS400 2.75mm FOB China USD/mt",
"HR Coil SS400 3.0mm In warehouse Bangkok THB/mt",
"HR Coil SS400 3.0mm In warehouse Hanoi VND/mt",
"HR Coil SS400 3.0mm In warehouse Kuala Lumpur MYR/mt",
"HR Coil SS400 5.5mm FOB China USD/mt",
"Holmium Oxide 99.5%min EXW China RMB/kg",
"Lanthanum Chloride 99.9%min EXW China RMB/mt",
"Lanthanum Metal 99%min EXW China RMB/mt",
"Lanthanum Metal 99%min FOB China USD/kg",
"Lanthanum Oxide 99.9%min EXW China RMB/mt"
],
"Pipe": [
"Seamless Pipe 20# ?219*6mm GB8163 In warehouse Guangzhou RMB/mt",
"Seamless Pipe 20# ?219*6mm GB8163 In warehouse Shanghai RMB/mt",
"Seamless Pipe 20# ?219*6mm GB8163 In warehouse Tianjin RMB/mt",
"Welded Pipe Q235 ?114*3.75mm In warehouse Guangzhou RMB/mt",
"Welded Pipe Q235 ?114*3.75mm In warehouse Shanghai RMB/mt",
"Welded Pipe Q235 ?114*3.75mm In warehouse Tianjin RMB/mt"
],
"Plate": [
"Medium Plate IS2062 16mm In warehouse Mumbai INR/mt",
"Medium Plate Q235B 10mm In warehouse Guangzhou RMB/mt",
"Medium Plate Q235B 10mm In warehouse Shanghai RMB/mt",
"Medium Plate Q235B 10mm In warehouse Tianjin RMB/mt",
"Medium Plate Q235B 16mm In warehouse Guangzhou RMB/mt",
"Medium Plate Q235B 16mm In warehouse Shanghai RMB/mt",
"Medium Plate Q235B 16mm In warehouse Tianjin RMB/mt",
"Medium Plate S275 JR 16mm In warehouse Istanbul USD/mt",
"Medium Plate SS400 10mm FOB China USD/mt",
"Medium Plate SS400 16mm FOB China USD/mt",
"Medium Plate SS400 16mm In warehouse Bangkok THB/mt",
"Medium Plate SS400 16mm In warehouse Hanoi VND/mt",
"Medium Plate SS400 16mm In warehouse Kuala Lumpur MYR/mt"
],
"Rebar": [
"Rebar BS4449 GR500 12mm In warehouse Kuala Lumpur MYR/mt",
"Rebar BS4449 GR500 16mm In warehouse Kuala Lumpur MYR/mt",
"Rebar CB400V/SD390 ?14-32mm In warehouse Hanoi VND/mt",
"Rebar HRB400 12mm FOB China USD/mt",
"Rebar HRB400 20mm FOB China USD/mt",
"Rebar HRB400 20mm In warehouse Guangzhou RMB/mt",
"Rebar HRB400 20mm In warehouse Shanghai RMB/mt",
"Rebar HRB400 20mm In warehouse Tianjin RMB/mt",
"TMT Bar Fe500 12mm In warehouse Mumbai INR/mt",
"TMT Bar Fe500 16mm In warehouse Mumbai INR/mt"
],
"Sections": [
"Angle Steel Q235 50*50*5mm In warehouse Guangzhou RMB/mt",
"Angle Steel Q235 50*50*5mm In warehouse Shanghai RMB/mt",
"Angle Steel Q235 50*50*5mm In warehouse Tianjin RMB/mt",
"Channel Steel Q235 160*65*8.5mm In warehouse Guangzhou RMB/mt",
"Channel Steel Q235 160*65*8.5mm In warehouse Shanghai RMB/mt",
"Channel Steel Q235 160*65*8.5mm In warehouse Tianjin RMB/mt",
"Dysprosium Metal 99.5%min EXW China RMB/kg",
"Dysprosium Metal 99.5%min FOB China USD/kg",
"Dysprosium Oxide 99.5%min EXW China RMB/kg",
"Dysprosium Oxide 99.5%min FOB China USD/kg",
"Erbium Oxide 99.5%min EXW China RMB/kg",
"Erbium Oxide 99.5%min FOB China USD/kg",
"Europium Metal 99.5%min FOB China USD/kg",
"Europium Oxide 99.999%min EXW China RMB/kg",
"Europium Oxide 99.999%min FOB China USD/kg",
"Ferro-dysprosium 80% EXW China RMB/kg",
"Ferro-dysprosium 80% FOB China USD/kg",
"H-beam Steel Q235 200*200mm In warehouse Guangzhou RMB/mt",
"H-beam Steel Q235 200*200mm In warehouse Shanghai RMB/mt",
"H-beam Steel Q235 200*200mm In warehouse Tianjin RMB/mt",
"H-beam Steel Q235 300*300mm In warehouse Guangzhou RMB/mt",
"H-beam Steel Q235 300*300mm In warehouse Shanghai RMB/mt",
"H-beam Steel Q235 300*300mm In warehouse Tianjin RMB/mt",
"I-beam Steel Q235 250*118*10mm In warehouse Guangzhou RMB/mt",
"I-beam Steel Q235 250*118*10mm In warehouse Shanghai RMB/mt",
"I-beam Steel Q235 250*118*10mm In warehouse Tianjin RMB/mt"
],
"Strip": [
"HR Strip Q195 295*2.5mm In warehouse Guangzhou RMB/mt",
"HR Strip Q195 295*2.5mm In warehouse Shanghai RMB/mt",
"HR Strip Q195 295*2.5mm In warehouse Tianjin RMB/mt",
"HR Strip Q195 685*2.75mm In warehouse Guangzhou RMB/mt",
"HR Strip Q235B 685*5.5mm In warehouse Shanghai RMB/mt",
"HR Strip Q235B 685*5.5mm In warehouse Tianjin RMB/mt"
],
"Wire Rod": [
"LaCe Mischmetal Ce/RE 65%min, Fe 0.3%max EXW China RMB/mt",
"LaCe Mischmetal Ce/RE 65%min, Fe 1%max EXW China RMB/mt",
"LaCe Mischmetal Ce/RE 65%min, Fe 1%max FOB China USD/kg",
"Wire Rod JIS SWRM15 ?6.5mm In warehouse Bangkok THB/mt",
"Wire Rod Q195 6.5mm FOB China USD/mt",
"Wire Rod Q235 8mm In warehouse Guangzhou RMB/mt",
"Wire Rod Q235 8mm In warehouse Shanghai RMB/mt",
"Wire Rod Q235 8mm In warehouse Tianjin RMB/mt",
"Wire Rod SAE1008 5.5mm In warehouse Kuala Lumpur MYR/mt",
"Wire Rod SAE1008 5.5mm In warehouse Mumbai INR/mt",
"Wire Rod SAE1008 6.5mm FOB China USD/mt"
]
},
"Ferroalloys": {
"Calcium Silicon": [
"Calcium-silicon 28-55 EXW China RMB/mt",
"Calcium-silicon 28-55 FOB China USD/mt",
"Calcium-silicon 30-60 EXW China RMB/mt",
"Calcium-silicon 30-60 FOB China USD/mt"
],
"Chrome Ore": [
"Chrome Conc. 40%min FOB S.A. USD/mt",
"Chrome Conc. 42%min FOB S.A. USD/mt",
"Chrome Conc. 46%min FOB Turkey USD/mt",
"Chrome Conc. S.A. 40%min CIF China USD/mt",
"Chrome Conc. S.A. 40%min In port China RMB/dmtu",
"Chrome Conc. S.A. 42%min CIF China USD/mt",
"Chrome Conc. S.A. 42%min In port China RMB/dmtu",
"Chrome Conc. Turkish 46%min CIF China USD/mt",
"Chrome Conc. Turkish 46%min In port China RMB/dmtu",
"Chrome Conc. Zimbabwean 48%min CIF China USD/mt",
"Chrome Conc. Zimbabwean 48%min In port China RMB/dmtu",
"Chrome Lump 40%min FOB Turkey USD/mt",
"Chrome Lump S.A. 38%min CIF China USD/mt",
"Chrome Lump S.A. 38%min In port China RMB/dmtu",
"Chrome Lump Turkish 40%min CIF China USD/mt",
"Chrome Lump Turkish 40%min In port China RMB/dmtu"
],
"Chromium Silicon": [
"Silico-chrome Si 42%min, Cr 30%min EXW China RMB/mt"
],
"Ferroboron": [
"Ferro-boron B 18%min EXW China RMB/mt",
"Ferro-boron B 18%min FOB China USD/mt",
"Ferro-boron B 19%min EXW China RMB/mt"
],
"Ferrochrome": [
"Ferro-chrome Cr 52%min, C 8%max EXW China RMB/mt 50-basis",
"Ferro-chrome Cr 55%min, C 0.1%max EXW China RMB/mt 60-basis",
"Ferro-chrome Cr 55%min, C 0.25%max EXW China RMB/mt 60-basis",
"Ferro-chrome Cr 60%min, C 0.1%max FOB China USD/lb Cr",
"Ferro-chrome Cr 60%min, C 0.1%max In warehouse Pittsburgh USD/lb Cr",
"Ferro-chrome Cr 60%min, C 0.1%max In warehouse Rotterdam USD/lb Cr",
"Ferro-chrome Cr 60%min, C 8%max In warehouse Pittsburg USD/lb Cr",
"Ferro-chrome Cr 60%min, C 8%max In warehouse Rotterdam USD/lb Cr",
"Ferro-chrome Cr 60%min, C 8.5%max In warehouse Russia RUB/kg",
"Ferro-chrome Cr 65%min, C 0.1%max In warehouse Russia RUB/kg",
"Ferro-chrome Indian Cr 58%min, C 8%max CIF China USD/lb Cr",
"Ferro-chrome Indian Cr 58%min, C 8%max In port China RMB/mt 50-basis",
"Ferro-chrome Kazakhstan Cr 68%min, C 8.5%max CIF China USD/lb Cr",
"Ferro-chrome Kazakhstan Cr 68%min, C 8.5%max In port China RMB/mt 50-basis",
"Ferro-chrome S.A. Cr 50%min, C 8%max CIF China USD/lb Cr",
"Ferro-chrome S.A. Cr 50%min, C 8%max In port China RMB/mt 50-basis"
],
"Ferromanganese": [
"Ferro-manganese Mn 65%min, C 7%max EXW China RMB/mt",
"Ferro-manganese Mn 75%min, C 2%max EXW China RMB/mt",
"Ferro-manganese Mn 75%min, C 8%max EXW China RMB/mt",
"Ferro-manganese Mn 78%min, C 1.5%max EXW China RMB/mt",
"Ferro-manganese Mn 78%min, C 7%max In warehouse Russia RUB/kg",
"Ferro-manganese Mn 78%min, C 8%max Delivered Europe EUR/mt",
"Ferro-manganese Mn 78%min, C 8%max In warehouse Pittsburgh USD/gt",
"Ferro-manganese Mn 80%min, C 0.7%max EXW China RMB/mt"
],
"Ferromolybdenum": [
"Ferro-molybdenum 60%min Delivered India INR/kg",
"Ferro-molybdenum 60%min EXW China RMB/mt",
"Ferro-molybdenum 60%min In port South Korea USD/kg Mo",
"Ferro-molybdenum 60%min In warehouse Russia RUB/kg",
"Ferro-molybdenum 65%min In warehouse Rotterdam USD/kg Mo",
"Ferro-molybdenum 65%min In warehouse US USD/kg Mo",
"Molybdenum Oxide 57%min In port South Korea USD/lb",
"Molybdenum Oxide 57%min In warehouse Rotterdam USD/lb"
],
"Ferronickel": [
"Ferro-nickel 1.5%min EXW China RMB/mt",
"Ferro-nickel 10%min EXW China RMB/mtu",
"Nickel Ore Philippine 1.5%min CIF China USD/mt",
"Nickel Ore Philippine 1.8%min In port China RMB/mt"
],
"Ferroniobium": [
"Ferro-niobium 50-A EXW China RMB/mt",
"Ferro-niobium 60-A EXW China RMB/mt",
"Ferro-niobium Brazilian 66% Delivered China RMB/mt",
"Ferro-niobium Brazilian 66% In warehouse Rotterdam USD/kg Nb",
"Niobium Conc. Nb2O5 50%min, Ta2O5 5%min, CIF China USD/lb Oxides",
"Niobium Pentoxide 99.5%min EXW China RMB/kg"
],
"Ferrophosphorus": [
"Ferro-phosphorus 23%min FOB China USD/mt"
],
"Ferrosilicon": [
"Ferro-silicon 65%min FOB Russia USD/mt",
"Ferro-silicon 65%min In warehouse Russia RUB/kg",
"Ferro-silicon 70%min EXW India INR/mt",
"Ferro-silicon 72%min EXW China RMB/mt",
"Ferro-silicon 72%min Ex-VAT FOB China USD/mt",
"Ferro-silicon 72%min FOB China USD/mt",
"Ferro-silicon 75%min EXW China RMB/mt",
"Ferro-silicon 75%min Ex-VAT FOB China USD/mt",
"Ferro-silicon 75%min FOB China USD/mt",
"Ferro-silicon 75%min In warehouse Pittsburgh USD/lb Si",
"Ferro-silicon 75%min In warehouse Rotterdam EUR/mt"
],
"Ferrotitanium": [
"Ferro-titanium 30% EXW China RMB/mt 25-basis",
"Ferro-titanium 30% EXW India INR/kg",
"Ferro-titanium 70% (scrap) EXW China RMB/mt",
"Ferro-titanium 70% In port India INR/kg",
"Ferro-titanium 70%min Delivered US USD/kg Ti",
"Ferro-titanium 70%min EXW Russia RUB/kg",
"Ferro-titanium 70%min FOB Russia USD/kg Ti",
"Ferro-titanium 70%min In warehouse Rotterdam USD/kg Ti"
],
"Ferrotungsten": [
"Ferro-tungsten 70%min EXW China RMB/mt",
"Ferro-tungsten 75%min EXW China RMB/mt",
"Ferro-tungsten 75%min FOB China USD/kg W",
"Ferro-tungsten 75%min FOB Russia USD/kg W",
"Ferro-tungsten 75%min In warehouse Rotterdam USD/kg W"
],
"Ferrovanadium": [
"Ferro-vanadium 50%min CIF China USD/kg V",
"Ferro-vanadium 50%min Delivered China RMB/mt",
"Ferro-vanadium 50%min In port China RMB/mt",
"Ferro-vanadium 50%min In warehouse Russia RUB/kg",
"Ferro-vanadium 80%min FOB China USD/kg V",
"Ferro-vanadium 80%min In warehouse Pittsburgh USD/kg V",
"Ferro-vanadium 80%min In warehouse Rotterdam USD/kg V",
"Vanadium Nitride N 14%min EXW China RMB/mt",
"Vanadium Pentoxide Flake 98%min In warehouse Rotterdam USD/lb V2O5"
],
"Manganese Ore": [
"Manganese Ore 38%min FOB S.A. USD/dmtu",
"Manganese Ore Australian 45%min In Qinzhou Port RMB/mtu",
"Manganese Ore Australian 45%min In Tianjin Port RMB/mtu",
"Manganese Ore Brazilian 44%min In Qinzhou Port RMB/mtu",
"Manganese Ore Brazilian 44%min In Tianjin Port RMB/mtu",
"Manganese Ore Carbonate, Mn 13%min EXW China RMB/mtu",
"Manganese Ore Gabonese 44%min In Qinzhou Port RMB/mtu",
"Manganese Ore Gabonese 44%min In Tianjin Port RMB/mtu",
"Manganese Ore S.A. 36%min In Qinzhou Port RMB/mtu",
"Manganese Ore S.A. 36%min In Tianjin Port RMB/mtu",
"Manganese Ore S.A. 37%min In Qinzhou Port RMB/mtu",
"Manganese Ore S.A. 37%min In Tianjin Port RMB/mtu"
],
"Silicomanganese": [
"Silico-manganese 60/14 EXW China RMB/mt",
"Silico-manganese 60/14 FOB India USD/mt",
"Silico-manganese 65/17 Delivered Europe EUR/mt",
"Silico-manganese 65/17 EXW China RMB/mt",
"Silico-manganese 65/17 FOB India USD/mt",
"Silico-manganese 65/17 In warehouse Pittsburgh USD/lb",
"Silico-manganese 65/17 In warehouse Russia RUB/kg"
]
},
"Minor Metals": {
"Antimony": [
"Antimony Conc. 50%min Delivered China RMB/mt Sb",
"Antimony Ingot 99.65%min EXW China RMB/mt",
"Antimony Ingot 99.65%min In port India INR/kg",
"Antimony Ingot 99.65%min In warehouse Baltimore USD/lb",
"Antimony Ingot 99.65%min In warehouse Rotterdam USD/mt",
"Antimony Ingot 99.85%min EXW China RMB/mt",
"Antimony Trioxide 99.5%min Delivered China RMB/mt",
"Antimony Trioxide 99.5%min FOB China USD/mt",
"Antimony Trioxide 99.5%min In warehouse Baltimore USD/lb",
"Antimony Trioxide 99.5%min In warehouse Rotterdam EUR/kg",
"Antimony Trioxide 99.8%min Delivered China RMB/mt",
"Ethylene Glycol Antimony 57%min Delivered China RMB/mt"
],
"Arsenic": [
"Arsenic Metal 99%min EXW China RMB/mt",
"Arsenic Metal 99%min FOB China USD/mt",
"Arsenic Trioxide 99%min EXW China RMB/mt",
"Arsenic Trioxide 99%min FOB China USD/mt"
],
"Bismuth": [
"Bismuth Ingot 99.99%min Delivered Europe USD/lb",
"Bismuth Ingot 99.99%min Delivered India INR/kg",
"Bismuth Ingot 99.99%min Delivered US USD/lb",
"Bismuth Ingot 99.99%min EXW China RMB/mt",
"Bismuth Ingot 99.99%min FOB China USD/lb",
"Bismuth Oxide 99.9%min EXW China RMB/mt"
],
"Cadmium": [
"Cadmium Ingot 99.99%min Delivered India INR/kg",
"Cadmium Ingot 99.99%min EXW China RMB/mt",
"Cadmium Ingot 99.99%min In warehouse Baltimore USD/lb",
"Cadmium Ingot 99.99%min In warehouse Rotterdam USD/lb"
],
"Calcium": [
"Calcium Granule Electrolytic 98.5%min EXW China RMB/mt",
"Calcium Granule Electrolytic 98.5%min FOB China USD/mt",
"Calcium Granule Reductive 98%min EXW China RMB/mt",
"Calcium Granule Reductive 98%min FOB China USD/mt",
"Calcium Ingot Reductive 98%min EXW China RMB/mt",
"Fluorspar 97%min Delivered China RMB/mt"
],
"Chromium": [
"Chromic Anhydride 99.8%min EXW China RMB/mt",
"Chromium Metal 99%min EXW China RMB/mt",
"Chromium Metal 99%min FOB China USD/mt",
"Chromium Oxide Green 99%min EXW China RMB/mt"
],
"Cobalt": [
"Cobalt Carbonate 46%min Delivered China RMB/mt",
"Cobalt Chloride 24%min Delivered China RMB/mt",
"Cobalt Conc. 6-8% CIF China USD/lb Co",
"Cobalt Intermediate 20-30% CIF China USD/lb Co",
"Cobalt Metal 99.8%min Delivered China RMB/kg",
"Cobalt Metal 99.8%min In warehouse Baltimore USD/lb",
"Cobalt Metal 99.8%min In warehouse Rotterdam USD/lb",
"Cobalt Monoxide 78%min Delivered China RMB/kg",
"Cobalt Oxide 72%min Delivered China RMB/kg",
"Cobalt Oxide 72%min In warehouse Rotterdam EUR/kg",
"Cobalt Powder 99.5%min Delivered China RMB/kg",
"Cobalt Sulfate 20.5%min Delivered China RMB/mt",
"Cobalt Tetroxide 73.5%min Delivered China RMB/kg",
"Lithium Cobaltate Co 60%min Delivered China RMB/kg",
"NCM 523 Delivered China RMB/mt",
"NCM 622 Delivered China RMB/mt",
"Samarium Cobalt 30H EXW China RMB/kg"
],
"Gallium": [
"Gallium Metal 99.99%min EXW China RMB/kg",
"Gallium Metal 99.99%min FOB China USD/kg",
"Gallium Metal 99.99999%min EXW China RMB/kg"
],
"Germanium": [
"Germanium Dioxide 99.99%min Delivered US USD/kg",
"Germanium Dioxide 99.99%min EXW China RMB/kg",
"Germanium Dioxide 99.99%min FOB China USD/kg",
"Germanium Metal 99.99%min Delivered Europe USD/kg",
"Germanium Metal 99.99%min Delivered US USD/kg",
"Germanium Metal 99.99%min EXW China RMB/kg",
"Germanium Metal 99.99%min FOB China USD/kg"
],
"Indium": [
"Indium Ingot 99.995%min Delivered Europe USD/kg",
"Indium Ingot 99.995%min Delivered US USD/kg",
"Indium Ingot 99.995%min EXW China RMB/kg",
"Indium Ingot 99.995%min FOB China USD/kg"
],
"Lithium": [
"LNCMO 523 Delivered China RMB/kg",
"LNCMO 622 Delivered China RMB/kg",
"Lithium Carbonate 99%min Delivered China RMB/mt",
"Lithium Carbonate 99.5%min CIF China USD/kg",
"Lithium Carbonate 99.5%min Delivered China RMB/mt",
"Lithium Carbonate 99.5%min Delivered EU USD/kg",
"Lithium Carbonate 99.5%min Delivered US USD/kg",
"Lithium Carbonate 99.5%min FOB South America USD/kg",
"Lithium Chloride 99.3%min Delivered China RMB/mt",
"Lithium Hydroxide Monohydrate LiOH 56.5%min Delivered China RMB/mt",
"Lithium Hydroxide Monohydrate LiOH 56.5%min, Magnets 0.0001%max Delivered China RMB/mt",
"Lithium Hydroxide Monohydrate LiOH 56.5%min, Magnets 0.0001%max Delivered South Korea USD/kg",
"Lithium Hydroxide Monohydrate LiOH 56.5%min, Magnets 0.0001%max FOB China USD/kg",
"Lithium Iron Phosphate Li 3.9%min Delivered China RMB/mt",
"Lithium Manganate Mn 58% Delivered China RMB/mt",
"Lithium Metal 99%min Delivered China RMB/mt",
"Lithium Metal 99.9%min Delivered China RMB/mt",
"Spodumene Li2O 6%min CIF China USD/mt"
],
"Magnesium": [
"Magnesium Alloy AM50A EXW China RMB/mt",
"Magnesium Alloy AM50A FOB China USD/mt",
"Magnesium Alloy AM60B EXW China RMB/mt",
"Magnesium Alloy AM60B FOB China USD/mt",
"Magnesium Alloy AZ91D EXW China RMB/mt",
"Magnesium Alloy AZ91D FOB China USD/mt",
"Magnesium Ingot 99.9%min EXW China RMB/mt",
"Magnesium Ingot 99.9%min Ex-VAT FOB China USD/mt",
"Magnesium Ingot 99.9%min FOB China USD/mt",
"Magnesium Ingot 99.9%min In port India INR/kg",
"Magnesium Ingot 99.9%min In warehouse Rotterdam USD/mt",
"Magnesium Ingot 99.9%min in warehouse Russia RUB/kg",
"Magnesium Ingot 99.95%min EXW China RMB/mt",
"Magnesium Powder 99.9%min EXW China RMB/mt",
"Magnesium Powder 99.9%min FOB China USD/mt"
],
"Manganese": [
"Manganese Briquette 97%min FOB China USD/mt",
"Manganese Dioxide Alkaline 91%min EXW China RMB/mt",
"Manganese Dioxide Carbon 91%min EXW China RMB/mt",
"Manganese Flake 99.7%min CIF India USD/mt",
"Manganese Flake 99.7%min EXW China RMB/mt",
"Manganese Flake 99.7%min Ex-VAT FOB China USD/mt",
"Manganese Flake 99.7%min FOB China USD/mt",
"Manganese Flake 99.7%min In warehouse Rotterdam USD/mt",
"Manganese Lump 96.5%min FOB China USD/mt",
"Manganese Lump 96.5%min In warehouse Russia RUB/kg",
"Manganese Sulfate Mn 32%min EXW China RMB/t"
],
"Mercury": [
"Mercury Metal 99.9%min Ex-VAT EXW China RMB/kg",
"Mercury Metal 99.999%min Ex-VAT EXW China RMB/kg"
],
"Molybdenum": [
"Ammonium Heptamolybdate 54%min EXW China RMB/mt",
"Ammonium Tetrathiomolybdate 56%min EXW China RMB/mt",
"Molybdenum Bar 99.9%min EXW China RMB/kg",
"Molybdenum Conc. 45%min EXW China RMB/mtu",
"Molybdenum Oxide 57%min In port India USD/lb",
"Molybdenum Oxide 50%min EXW China RMB/mtu",
"Molybdenum Oxide 57%min CIF China USD/lb Mo",
"Molybdenum Oxide 57%min In port South Korea USD/lb",
"Molybdenum Oxide 57%min In warehouse Rotterdam USD/lb",
"Molybdenum Oxide 99.9%min EXW China RMB/mt",
"Molybdenum Powder 99.95%min EXW China RMB/kg",
"Molybdenum Scrap Cutting Scrap 99.95%min Ex-VAT EXW China RMB/kg",
"Molybdenum Scrap Wire Scrap 99.95%min Ex-VAT EXW China RMB/kg",
"Sodium Molybdate 99%min EXW China RMB/mt"
],
"Niobium": [
"Niobium Conc. Nb2O5 50%min, Ta2O5 5%min, CIF China USD/lb Oxides",
"Niobium Pentoxide 99.5%min EXW China RMB/kg",
"Niobium Pentoxide 99.5%min FOB China USD/kg",
"Niobium Pentoxide 99.99%min EXW China RMB/kg",
"Niobium Pentoxide 99.99%min FOB China USD/kg"
],
"Rhenium": [
"Rhenium APR 99.99%min EXW China RMB/kg"
],
"Selenium": [
"Selenium Dioxide 98%min Delivered China RMB/kg",
"Selenium Powder 99.5%min In warehouse Rotterdam USD/lb",
"Selenium Powder 99.9%min CIF China USD/lb",
"Selenium Powder 99.9%min Delivered China RMB/kg",
"Selenium Powder 99.9%min Delivered India INR/kg",
"Selenium Powder 99.9%min Delivered US USD/lb",
"Selenium Powder 99.9%min FOB Russia USD/kg",
"Selenium Powder 99.95%min Delivered China RMB/kg"
],
"Silicon": [
"Polysilicon 9N Delivered China RMB/kg",
"Silicon Metal 2-2-02 Delivered China RMB/mt",
"Silicon Metal 2-2-02 Ex-VAT FOB China USD/mt",
"Silicon Metal 2-2-02 FOB China USD/mt",
"Silicon Metal 3-3-03 Delivered China RMB/mt",
"Silicon Metal 3-3-03 Ex-VAT FOB China USD/mt",
"Silicon Metal 3-3-03 FOB China USD/mt",
"Silicon Metal 3-3-03 P 50ppm max Delivered China RMB/mt",
"Silicon Metal 3-3-03 P 50ppm max Ex-VAT FOB China USD/mt",
"Silicon Metal 3-3-03 P 50ppm max FOB China USD/mt",
"Silicon Metal 4-2-1 Delivered China RMB/mt",
"Silicon Metal 4-4-1 Delivered China RMB/mt",
"Silicon Metal 4-4-1 Delivered Europe EUR/mt",
"Silicon Metal 4-4-1 Ex-VAT FOB China USD/mt",
"Silicon Metal 4-4-1 FOB China USD/mt",
"Silicon Metal 4-4-1 In port India INR/kg",
"Silicon Metal 5-5-3 Delivered China RMB/mt",
"Silicon Metal 5-5-3 Delivered Europe EUR/mt",
"Silicon Metal 5-5-3 Ex-VAT FOB China USD/mt",
"Silicon Metal 5-5-3 FOB China USD/mt",
"Silicon Metal 5-5-3 In port India INR/kg",
"Silicon Metal 98.5%min Delivered US USD/lb",
"Silicon Powder 99%min Delivered China RMB/mt"
],
"Strontium": [
"Strontium Metal 99%min EXW China RMB/mt",
"Strontium Metal 99%min FOB China USD/mt"
],
"Tantalum": [
"Tantalum Conc. Ta2O5 30%min CIF China USD/lb Ta2O5",
"Tantalum Conc. Ta2O5 30%min in warehouse Rotterdam USD/lb Ta2O5",
"Tantalum Metal 99.95%min Delivered US USD/kg",
"Tantalum Metal 99.95%min EXW China RMB/kg",
"Tantalum Metal 99.95%min FOB China USD/kg",
"Tantalum Pentoxide 99.5%min EXW China RMB/kg",
"Tantalum Pentoxide 99.5%min FOB China USD/kg",
"Tantalum Pentoxide 99.99%min EXW China RMB/kg",
"Tantalum Pentoxide 99.99%min FOB China USD/kg"
],
"Tellurium": [
"Tellurium Metal 99.9%min In warehouse Rotterdam USD/kg",
"Tellurium Metal 99.99%min EXW China RMB/kg",
"Tellurium Metal 99.99%min In warehouse Rotterdam USD/kg"
],
"Titanium": [
"Ilmenite Conc Australian TiO2 55-58%min, Fe 27%min CIF China USD/mt",
"Ilmenite Conc Kenyan TiO2 48%min, Fe2O3 20%max CIF China USD/mt",
"Ilmenite Conc Mozambican TiO2 52%min, Fe2O3 25%max CIF China USD/mt",
"Ilmenite Conc TiO2 46%min; Fe2O3 8%max; Ex-VAT EXW China RMB/mt",
"Ilmenite Conc TiO2 50%min; Fe 30%min; In port China RMB/mt",
"Ilmenite Conc TiO2 50%min; Fe2O3 14%max; In port China RMB/mt",
"Ilmenite Conc TiO2 52%min, Fe2O3 25%max FOB Vietnam USD/mt",
"Reduced Ilmenite TiO2 56%min EXW China RMB/mt",
"Rutile Conc TiO2 95%min In port China RMB/mt",
"Titanium Dioxide A TiO2 98%min Delivered China RMB/mt",
"Titanium Dioxide R TiO2 93%min Delivered China RMB/mt",
"Titanium Dioxide R TiO2 93%min FOB China USD/mt",
"Titanium Pipe TA2 25.4x1mm EXW China RMB/kg",
"Titanium Plate TA2 2mm EXW China RMB/kg",
"Titanium Slag TiO2 74%min EXW China RMB/mt",
"Titanium Slag TiO2 90%min EXW China RMB/mt",
"Titanium Sponge 99.7%min EXW China RMB/mt",
"Titanium Tetrachloride 99.99%min EXW China RMB/mt"
],
"Tungsten": [
"Sodium Tungstate WO3 68%min EXW China RMB/mt",
"Thorium Tungsten Electrode ThO2 1.7-2.2% EXW China RMB/kg",
"Tungsten APT 88.5%min EXW China RMB/mt",
"Tungsten APT 88.5%min FOB China USD/mtu",
"Tungsten APT 88.5%min In warehouse Rotterdam USD/mtu",
"Tungsten Bar W-4 99.9%min EXW China RMB/kg",
"Tungsten Bar W-4 99.9%min FOB China USD/kg",
"Tungsten Carbide 99.8%min 2.5-7.0?m EXW China RMB/kg",
"Tungsten Carbide 99.8%min 2.5-7.0?m FOB China USD/kg",
"Tungsten Ore WO3 50%min FOB Africa USD/mtu",
"Tungsten Ore WO3 65%min EXW China RMB/mt",
"Tungsten Oxide WO3 99.95%min EXW China RMB/mt",
"Tungsten Oxide WO3 99.95%min FOB China USD/mtu",
"Tungsten Powder 99.95%min 2.5-7.0?m EXW China RMB/kg",
"Tungsten Scrap Anvil 90%min Ex-VAT Delivered China RMB/kg",
"Tungsten Scrap CNC Cutting Blades 70%min Ex-VAT Delivered China RMB/kg",
"Tungsten Scrap Drills 70%min Ex-VAT Delivered China RMB/kg",
"Tungsten Scrap Grinding Swarf W 70%min, Co 5%min Ex-VAT Delivered China RMB/mtu"
],
"Vanadium": [
"Ammonium Metavanadate 98%min EXW China RMB/mt",
"Vanadium Nitride N 14%min EXW China RMB/mt",
"Vanadium Pentoxide Flake 92%min CIF China USD/lb V2O5",
"Vanadium Pentoxide Flake 92%min In port China RMB/mt",
"Vanadium Pentoxide Flake 98%min CIF China USD/lb V2O5",
"Vanadium Pentoxide Flake 98%min EXW China RMB/mt",
"Vanadium Pentoxide Flake 98%min In port China RMB/mt",
"Vanadium Pentoxide Flake 98%min In warehouse Rotterdam USD/lb V2O5",
"Vanadium Pentoxide Powder 98%min EXW China RMB/mt",
"Vanadium Pentoxide Powder 99%min EXW China RMB/mt",
"Vanadium Pentoxide Powder 99.5%min EXW China RMB/mt"
],
"Zirconium": [
"Fused Zirconia Zr(Hf)O2 98.5%min EXW China RMB/mt",
"Zircon Sand Australian Zr(Hf)O2 66%min In port China RMB/mt",
"Zircon Sand Australian Zr(Hf)O2 66%min In port China USD/mt",
"Zircon Sand Indonesian Zr(Hf)O2 66%min CIF China USD/mt",
"Zircon Sand S.A. Zr(Hf)O2 65%min In port China RMB/mt",
"Zircon Sand S.A. Zr(Hf)O2 65%min In port China USD/mt",
"Zircon Sand S.A. Zr(Hf)O2 66%min In port China USD/mt",
"Zircon Sand Zr(Hf)O2 65%min EXW China RMB/mt",
"Zirconium Carbonate Zr(Hf)O2 40%min EXW China RMB/mt",
"Zirconium Carbonate Zr(Hf)O2 40%min, Cl 50ppm EXW China RMB/mt",
"Zirconium Oxychloride Zr(Hf)O2 36%min EXW China RMB/mt",
"Zirconium Silicate Zr(Hf)O2 64.5%min EXW China RMB/mt",
"Zirconium Sponge Zr+Hf 99.4%min EXW China RMB/kg"
]
},
"Rare Earths": {
"Lutetium": [
"Lutetium Oxide 99.99%min China RMB/kg"
],
"Neodymium": [
"Cerium Carbonate TREO 45%min, CeO2/REO 100% EXW China RMB/mt",
"Cerium Carbonate TREO 45%min, CeO2/REO 100% FOB China USD/mt",
"Cerium Metal 99%min EXW China RMB/mt",
"Cerium Metal 99%min FOB China USD/kg",
"Cerium Oxide 99%min FOB China USD/mt",
"Cerium Oxide 99.9%min EXW China RMB/mt",
"Cerium Oxide 99.9%min In warehouse Rotterdam USD/kg",
"Cerium Oxide 99.99%min EXW China RMB/mt",
"LaCe Carbonate TREO 45%min, CeO2/REO 65±2% EXW China RMB/mt",
"LaCe Chloride TREO 45%min, CeO2/REO 65±2% EXW China RMB/mt",
"NdFeB Sintered Rough 35H EXW China RMB/kg",
"NdFeB Sintered Rough 35M EXW China RMB/kg",
"NdFeB Sintered Rough 45H EXW China RMB/kg",
"NdFeB Sintered Rough 45M EXW China RMB/kg",
"NdFeB Sintered Rough 48H EXW China RMB/kg",
"NdFeB Sintered Rough 50H EXW China RMB/kg",
"NdFeB Sintered Rough 50M EXW China RMB/kg",
"NdFeB Sintered Rough N35 EXW China RMB/kg",
"NdFeB Sintered Rough N45 EXW China RMB/kg",
"NdFeB Sintered Rough N52 EXW China RMB/kg",
"Neodymium Metal 99%min EXW China RMB/mt",
"Neodymium Metal 99%min FOB China USD/kg",
"Neodymium Oxide 99.5%min EXW China RMB/mt",
"Neodymium Oxide 99.5%min FOB China USD/mt",
"PrNd Mischmetal Pr 25%, Nd 75% EXW China RMB/mt",
"PrNd Mischmetal Pr 25%, Nd 75% FOB China USD/kg",
"PrNd Oxide Pr6O11 25%, Nd2O3 75% EXW China RMB/mt"
],
"Praseodymium": [
"Praseodymium Metal 99.5%min FOB China USD/kg",
"Praseodymium Oxide 99.5%min EXW China RMB/mt",
"Praseodymium Oxide 99.5%min FOB China USD/mt",
"Praseodymium Oxide 99.5%min In warehouse Rotterdam USD/kg"
],
"Samarium": [
"Samarium Cobalt 30H EXW China RMB/kg",
"Samarium Metal 99.5%min EXW China RMB/kg",
"Samarium Metal 99.5%min FOB China USD/kg",
"Samarium Oxide 99.9%min EXW China RMB/mt",
"Samarium Oxide 99.9%min FOB China USD/mt",
"SmEuGd Oxide TREO 220g/L min, Eu2O3/REO 8%min EXW China RMB/mt"
],
"Scandium": [
"Scandium Aluminum Sc 2% EXW China RMB/kg",
"Scandium Metal 99.99%min EXW China RMB/kg",
"Scandium Metal 99.999%min EXW China RMB/kg",
"Scandium Oxide 99.99%min EXW China RMB/kg"
],
"Terbium": [
"Terbium Metal 99.9%min EXW China RMB/kg",
"Terbium Metal 99.9%min FOB China USD/kg",
"Terbium Oxide 99.99%min EXW China RMB/kg",
"Terbium Oxide 99.99%min FOB China USD/kg"
],
"Ytterbium": [
"Ytterbium Oxide 99.99%min EXW China RMB/kg"
],
"Yttrium": [
"YEu Oxide Eu2O3/REO 6.6%min EXW China RMB/kg",
"YEu Oxide Eu2O3/REO 6.6%min FOB China USD/kg",
"Yttrium Metal 99.9%min EXW China RMB/kg",
"Yttrium Metal 99.9%min FOB China USD/kg",
"Yttrium Oxide 99.999%min EXW China RMB/mt",
"Yttrium Oxide 99.999%min FOB China USD/kg",
"Yttrium Oxide 99.999%min In warehouse Rotterdam USD/kg"
]
},
"Refractories": {
"Calcined Bauxite": [
"Calcined Bauxite 85%min 1-3mm EXW China RMB/mt",
"Calcined Bauxite 86%min 1-3mm FOB China USD/mt",
"Calcined Bauxite 87%min 1-3mm EXW China RMB/mt"
],
"Carbon": [
"Calcined Pet Coke S 3%max EXW China RMB/mt",
"Coal Tar Pitch Coking Value 53%min EXW China RMB/mt",
"Graphite Electrode H.P. D300mm FOB China USD/mt",
"Graphite Electrode H.P. D400mm EXW China RMB/mt",
"Graphite Electrode H.P. D450mm FOB China USD/mt",
"Graphite Electrode H.P. D500mm EXW China RMB/mt",
"Graphite Electrode R.P. D400mm EXW China RMB/mt",
"Graphite Electrode U.H.P. D500mm EXW China RMB/mt",
"Graphite Electrode U.H.P. D500mm FOB China USD/mt",
"Graphite Electrode U.H.P. D600mm EXW China RMB/mt",
"Graphite Electrode U.H.P. D600mm FOB China USD/mt",
"Pet Coke S 2%max EXW China RMB/mt",
"Pet Coke S 3%max EXW China RMB/mt",
"Prebaked Anode S 3%max EXW China RMB/mt",
"Prebaked Anode S 3%max FOB China USD/mt"
],
"Fused Alumina": [
"Calcined Alumina 99%min 1-3mm US USD/st",
"Fused Alumina Brown 95%min 1-3mm EXW China RMB/mt",
"Fused Alumina Brown 95%min 1-3mm EXW India INR/mt",
"Fused Alumina Brown 95%min 1-3mm FOB China USD/mt",
"Fused Alumina Brown 95%min 1-3mm In warehouse Rotterdam EUR/mt",
"Fused Alumina Brown 95%min 1-3mm US USD/st",
"Fused Alumina Brown 95%min F36 EXW China RMB/mt",
"Fused Alumina Brown 95%min F36 EXW India INR/mt",
"Fused Alumina Brown 95%min F36 FOB China USD/mt",
"Fused Alumina White 99%min 1-3mm EXW China RMB/mt",
"Fused Alumina White 99%min 1-3mm FOB China USD/mt",
"Fused Alumina White 99%min 1-3mm In warehouse Rotterdam EUR/mt",
"Fused Alumina White 99%min 1-3mm US USD/st",
"Fused Alumina White 99%min F36 EXW China RMB/mt",
"Fused Alumina White 99%min F36 FOB China USD/mt",
"Tabular Alumina 99%min 1-3mm US USD/st"
],
"Graphite": [
"Amorphous Graphite 75%min 0-3mm EXW China RMB/mt",
"Amorphous Graphite 80%min 20-50mm EXW China RMB/mt",
"Amorphous Graphite 80%min 200mesh min EXW China RMB/mt",
"Expansible Graphite 99 80 250 EXW China RMB/mt",
"Flake Graphite +195 EXW China RMB/mt",
"Flake Graphite +895 EXW China RMB/mt",
"Flake Graphite -190 EXW China RMB/mt",
"Flake Graphite -194 EXW China RMB/mt",
"Flake Graphite -194 FOB China USD/mt",
"Flake Graphite -199 EXW China RMB/mt",
"Spherical Graphite 99.95%min 17?m max EXW China RMB/mt"
],
"Magnesia": [
"Magnesia C.C. 85%min 200mesh EXW China RMB/mt",
"Magnesia C.C. 90%min 200mesh EXW China RMB/mt",
"Magnesia C.C. 90%min 200mesh FOB China USD/mt",
"Magnesia C.C. 97%min 0-5mm EXW China RMB/mt",
"Magnesia D.B. 90%min 3-15mm EXW China RMB/mt",
"Magnesia D.B. 90%min 3-15mm FOB China USD/mt",
"Magnesia D.B. 90%min 3-15mm In warehouse Rotterdam EUR/mt",
"Magnesia D.B. 90%min 3-15mm US USD/st",
"Magnesia D.B. 95%min 0-30mm EXW China RMB/mt",
"Magnesia D.B. 95%min 0-30mm FOB China USD/mt",
"Magnesia D.B. 95%min 0-30mm In warehouse Rotterdam EUR/mt",
"Magnesia D.B. 96.5%min 0-30mm EXW China RMB/mt",
"Magnesia D.B. 97%min 0-30mm EXW China RMB/mt",
"Magnesia D.B. 97%min 0-30mm FOB China USD/mt",
"Magnesia E.F. 97%min EXW China RMB/mt",
"Magnesia E.F. 97%min H-Ca EXW China RMB/mt",
"Magnesia E.F. 97%min H-Ca FOB China USD/mt",
"Magnesia E.F. 97.5%min Large Crystal EXW China RMB/mt",
"Magnesia E.F. 97.5%min Large Crystal FOB China USD/mt",
"Magnesite Lump 47%min EXW China RMB/mt",
"Magnesite Pellet 47%min EXW China RMB/mt"
],
"Silicon Carbide": [
"Silicon Carbide Black 88%min 0-10mm EXW China RMB/mt",
"Silicon Carbide Black 88%min 0-10mm FOB China USD/mt",
"Silicon Carbide Black 98%min 10-30cm EXW China RMB/mt",
"Silicon Carbide Black 98%min F16-100 EXW China RMB/mt",
"Silicon Carbide Black 98%min F16-100 FOB China USD/mt",
"Silicon Carbide Green 98%min F240 EXW China RMB/mt",
"Silicon Carbide Green 98%min F240 FOB China USD/mt",
"Silicon Carbide Green 98%min JIS2000 EXW China RMB/mt",
"Silicon Carbide Green 98%min JIS2000 FOB China USD/mt",
"Silicon Carbide Green 98.5%min 10-30cm EXW China RMB/mt",
"Silicon Carbide Black 88%min 0-10mm In warehouse Rotterdam EUR/mt"
]
},
"Stainless & Special": {
"Bearing Steel": [
"Cold Heading Wire 22A ?6.5mm In warehouse Shanghai RMB/mt",
"Electrical Steel Grain Oriented 130 0.3*980mm In warehouse China RMB/mt",
"Electrical Steel Non-grain 470 0.5*1200mm In warehouse China RMB/mt",
"Electrical Steel Non-grain 800 0.5*1,200mm In warehouse China RMB/mt",
"Stainless Bar 321 60mm In warehouse Dainan RMB/mt",
"Stainless CR Coil 201/2B 1.0mm In warehouse China RMB/mt",
"Stainless CR Coil 304/2B 1.0mm In warehouse China RMB/mt",
"Stainless CR Coil 430/2B 1.0mm In warehouse China RMB/mt",
"Stainless HR Coil 201/No.1 6.0mm In warehouse China RMB/mt",
"Stainless HR Coil 304/No.1 6.0mm In warehouse China RMB/mt"
],
"Stainless Bar": [
"Stainless Bar 304 60mm In warehouse Dainan RMB/mt",
"Stainless Bar 321 60mm In warehouse Dainan RMB/mt"
],
"Stainless Pipe": [
"Stainless Seamless Pipe 304 108*4mm In warehouse Wenzhou RMB/mt"
],
"Stainless Scrap": [
"Stainless Scrap 304 Solid In warehouse Tokyo JPY/mt",
"Stainless Scrap 304 Solid In warehouse Dainan RMB/mt"
],
"Structural Steel": [
"Stainless CR Coil 304/2B 1.0mm In warehouse China RMB/mt"
]
},
"Steel Raw Materials": {
"Coal": [
"Coking Coal A 10%max, S 0.8%max EXW Hebei RMB/mt",
"Coking Coal A 10%max, S 0.8%max EXW Henan RMB/mt",
"Coking Coal A 10.5%max, S 0.8%max EXW Shanxi RMB/mt",
"Thermal Coal Q5000 In port Qinhuangdao RMB/mt",
"Thermal Coal Q5500 In port Qinhuangdao RMB/mt"
],
"Coke": [
"Met. Coke A 13%max, S 0.7%max EXW Hebei RMB/mt",
"Met. Coke A 13%max, S 0.7%max EXW Shandong RMB/mt",
"Met. Coke A 13%max, S 0.7%max EXW Shanxi RMB/mt"
],
"Iron": [
"Cast Iron Z18-22 EXW China RMB/mt",
"Pig Iron L8-10 EXW China RMB/mt",
"S.G. Cast Iron Q10-12 EXW China RMB/mt"
],
"Iron Ore": [
"Iron Ore Fine 65%min EXW Benxi RMB/dt",
"Iron Ore Fine 65%min EXW Laiwu RMB/dt",
"Iron Ore Fine 65%min Ex-VAT EXW Daixian RMB/wt",
"Iron Ore Fine 66%min Ex-VAT EXW Handan RMB/dt",
"Iron Ore Fine Australian 58%min In port China RMB/wt",
"Iron Ore Fine Australian 62%min CNF China USD/dt",
"Iron Ore Fine Australian 62%min In port China RMB/wt",
"Iron Ore Fine Brazilian 65%min In port China RMB/wt",
"Iron Ore Lump Australian 62%min In port China RMB/wt"
],
"Steel Billet": [
"Steel Billet HRB335 150x150mm EXW China RMB/mt",
"Steel Billet Q235 150x150mm EXW China RMB/mt"
],
"Steel Scrap": [
"Steel Scrap HMS 4-6mm EXW Shandong RMB/mt",
"Steel Scrap HMS 8mm min EXW Wuxi RMB/mt",
"Steel Scrap MMS 4-6mm EXW Guangzhou RMB/mt",
"Steel Scrap MMS 4-6mm EXW Tianjin RMB/mt",
"Steel Scrap MMS 6-10mm EXW Liaoning RMB/mt",
"Steel Scrap Process Scrap Delivered China RMB/mt"
]
}
},
"duplicate_title_placements": [
{
"title": "LaCe Chloride TREO 45%min, CeO2/REO 65±2% EXW China RMB/mt",
"placements": 2
},
{
"title": "LaCe Mischmetal Ce/RE 65%min, Fe 0.3%max EXW China RMB/mt",
"placements": 2
},
{
"title": "Molybdenum Oxide 57%min In port South Korea USD/lb",
"placements": 2
},
{
"title": "Molybdenum Oxide 57%min In warehouse Rotterdam USD/lb",
"placements": 2
},
{
"title": "Nickel Ore Philippine 1.5%min CIF China USD/mt",
"placements": 2
},
{
"title": "Niobium Conc. Nb2O5 50%min, Ta2O5 5%min, CIF China USD/lb Oxides",
"placements": 2
},
{
"title": "Niobium Pentoxide 99.5%min EXW China RMB/kg",
"placements": 2
},
{
"title": "Samarium Cobalt 30H EXW China RMB/kg",
"placements": 2
},
{
"title": "Stainless Bar 321 60mm In warehouse Dainan RMB/mt",
"placements": 2
},
{
"title": "Stainless CR Coil 304/2B 1.0mm In warehouse China RMB/mt",
"placements": 2
},
{
"title": "Vanadium Nitride N 14%min EXW China RMB/mt",
"placements": 2
},
{
"title": "Vanadium Pentoxide Flake 98%min In warehouse Rotterdam USD/lb V2O5",
"placements": 2
}
]
}

870
PRODUCT-CATALOG.md Normal file
View File

@ -0,0 +1,870 @@
# Product Catalog
Unique product names: 674
Group/category product entries: 686
Use /api/v1/metals/tree for this same catalog as JSON.
## Base Metals (34)
### Copper (6)
- Blister Copper TC 98.5%min Delivered China RMB/mt
- Copper Cathode 99.99%min Delivered China RMB/mt
- Copper Conc. 20%min Delivered China RMB/mt Cu
- Copper Conc. TC 25%min CIF China USD/mt
- Copper Rod TC ?8mm EXW China RMB/mt
- Copper Scrap Wire 99%min Delivered China RMB/mt
### Lead (10)
- Lead Conc. 60%min EXW China RMB/mt Pb
- Lead Conc. TC 50%min CIF China USD/mt
- Lead Ingot 99.994%min In warehouse Guangzhou RMB/mt
- Lead Ingot 99.994%min In warehouse Shanghai RMB/mt
- Lead Ingot Secondary 98%min EXW China RMB/mt
- Lead Ingot Secondary 98%min Ex-VAT EXW China RMB/mt
- Lead Ingot Secondary 99.994%min EXW China RMB/mt
- Lead Ingot Secondary 99.994%min Ex-VAT EXW China RMB/mt
- Lead Scrap Recycled Deep Cycle Battery Ex-VAT Delivered China RMB/mt
- Lead Scrap Recycled Start-type Battery Ex-VAT Delivered China RMB/mt
### Nickel (10)
- Nickel Cathode 99.96%min EXW China RMB/mt
- Nickel Cathode Norilsk 99.96%min In port China RMB/mt
- Nickel Ore Philippine 0.8%min CIF China USD/mt
- Nickel Ore Philippine 0.8%min In port China RMB/mt
- Nickel Ore Philippine 1.3%min CIF China USD/mt
- Nickel Ore Philippine 1.5%min CIF China USD/mt
- Nickel Ore Philippine 1.5%min In port China RMB/mt
- Nickel Ore Philippine 1.8%min CIF China USD/mt
- Nickel Sulfate Ni 22%min; Co 0.05%max Delivered China RMB/mt
- Nickel Sulfate Ni 22%min; Co 0.4%max Delivered China RMB/mt
### Tin (6)
- Tin Conc. 60%min Delivered China RMB/mtm
- Tin Conc. Burmese 20%min In warehouse MengA RMB/mtm
- Tin Conc. Burmese 30%min In warehouse MengA RMB/mtm
- Tin Ingot 99.9%min EXW China RMB/mt
- Tin Solder Wire Sn 99%min, Ag 0.3%min, Cu 0.7%max EXW China RMB/kg
- Tin Solder Wire Sn 99%min, Cu 0.7%max EXW China RMB/kg
### Zinc (2)
- Zinc Conc. 50%min EXW North China RMB/mt Zn
- Zinc Conc. TC 50% CIF China USD/mt
## Carbon Steel (120)
### Coated (9)
- HGI Coil DX51D 0.5mm FOB China USD/mt
- HGI Coil DX51D 0.5mm In warehouse Guangzhou RMB/mt
- HGI Coil DX51D 0.5mm In warehouse Shanghai RMB/mt
- HGI Coil DX51D 0.5mm In warehouse Tianjin RMB/mt
- HGI Coil DX51D 1.0mm FOB China USD/mt
- HGI Coil DX51D 1.0mm In warehouse Guangzhou RMB/mt
- HGI Coil DX51D 1.0mm In warehouse Shanghai RMB/mt
- HGI Coil DX51D 1.0mm In warehouse Tianjin RMB/mt
- HGI Coil IS513 0.5mm In warehouse Mumbai INR/mt
### Cold Rolled Coil (17)
- CR Coil DC01 1.0mm In warehouse Istanbul USD/mt
- CR Coil IS513 1.0mm In warehouse Mumbai INR/mt
- CR Coil SPCC 0.5mm FOB China USD/mt
- CR Coil SPCC 0.5mm In warehouse Guangzhou RMB/mt
- CR Coil SPCC 0.5mm In warehouse Shanghai RMB/mt
- CR Coil SPCC 0.5mm In warehouse Tianjin RMB/mt
- CR Coil SPCC 1.0mm FOB China USD/mt
- CR Coil SPCC 1.0mm In warehouse Guangzhou RMB/mt
- CR Coil SPCC 1.0mm In warehouse Hanoi VND/mt
- CR Coil SPCC 1.0mm In warehouse Kuala Lumpur MYR/mt
- CR Coil SPCC 1.0mm In warehouse Shanghai RMB/mt
- CR Coil SPCC 1.0mm In warehouse Tianjin RMB/mt
- LaCe Chloride TREO 45%min, CeO2/REO 65±2% EXW China RMB/mt
- LaCe Mischmetal Ce/RE 65%min, Fe 0.3%max EXW China RMB/mt
- Lanthanum Oxide 99.9%min FOB China USD/mt
- Lanthanum Oxide 99.999%min EXW China RMB/mt
- Lanthanum Oxide 99.999%min FOB China USD/kg
### Hot Rolled Coil (22)
- Ferro-gadolinium Gd 73% EXW China RMB/mt
- Ferro-holmium 80% EXW China RMB/kg
- Gadolinium Oxide 99.5%min EXW China RMB/mt
- Gadolinium Oxide 99.99%min EXW China RMB/mt
- HR Coil S275 JR 3.0mm In warehouse Istanbul USD/mt
- HR Coil IS2062 3.0mm In warehouse Mumbai INR/mt
- HR Coil Q235B 2.75mm In warehouse Guangzhou RMB/mt
- HR Coil Q235B 2.75mm In warehouse Shanghai RMB/mt
- HR Coil Q235B 2.75mm In warehouse Tianjin RMB/mt
- HR Coil Q235B 5.5mm In warehouse Guangzhou RMB/mt
- HR Coil Q235B 5.5mm In warehouse Shanghai RMB/mt
- HR Coil Q235B 5.5mm In warehouse Tianjin RMB/mt
- HR Coil SS400 2.75mm FOB China USD/mt
- HR Coil SS400 3.0mm In warehouse Bangkok THB/mt
- HR Coil SS400 3.0mm In warehouse Hanoi VND/mt
- HR Coil SS400 3.0mm In warehouse Kuala Lumpur MYR/mt
- HR Coil SS400 5.5mm FOB China USD/mt
- Holmium Oxide 99.5%min EXW China RMB/kg
- Lanthanum Chloride 99.9%min EXW China RMB/mt
- Lanthanum Metal 99%min EXW China RMB/mt
- Lanthanum Metal 99%min FOB China USD/kg
- Lanthanum Oxide 99.9%min EXW China RMB/mt
### Pipe (6)
- Seamless Pipe 20# ?219*6mm GB8163 In warehouse Guangzhou RMB/mt
- Seamless Pipe 20# ?219*6mm GB8163 In warehouse Shanghai RMB/mt
- Seamless Pipe 20# ?219*6mm GB8163 In warehouse Tianjin RMB/mt
- Welded Pipe Q235 ?114*3.75mm In warehouse Guangzhou RMB/mt
- Welded Pipe Q235 ?114*3.75mm In warehouse Shanghai RMB/mt
- Welded Pipe Q235 ?114*3.75mm In warehouse Tianjin RMB/mt
### Plate (13)
- Medium Plate IS2062 16mm In warehouse Mumbai INR/mt
- Medium Plate Q235B 10mm In warehouse Guangzhou RMB/mt
- Medium Plate Q235B 10mm In warehouse Shanghai RMB/mt
- Medium Plate Q235B 10mm In warehouse Tianjin RMB/mt
- Medium Plate Q235B 16mm In warehouse Guangzhou RMB/mt
- Medium Plate Q235B 16mm In warehouse Shanghai RMB/mt
- Medium Plate Q235B 16mm In warehouse Tianjin RMB/mt
- Medium Plate S275 JR 16mm In warehouse Istanbul USD/mt
- Medium Plate SS400 10mm FOB China USD/mt
- Medium Plate SS400 16mm FOB China USD/mt
- Medium Plate SS400 16mm In warehouse Bangkok THB/mt
- Medium Plate SS400 16mm In warehouse Hanoi VND/mt
- Medium Plate SS400 16mm In warehouse Kuala Lumpur MYR/mt
### Rebar (10)
- Rebar BS4449 GR500 12mm In warehouse Kuala Lumpur MYR/mt
- Rebar BS4449 GR500 16mm In warehouse Kuala Lumpur MYR/mt
- Rebar CB400V/SD390 ?14-32mm In warehouse Hanoi VND/mt
- Rebar HRB400 12mm FOB China USD/mt
- Rebar HRB400 20mm FOB China USD/mt
- Rebar HRB400 20mm In warehouse Guangzhou RMB/mt
- Rebar HRB400 20mm In warehouse Shanghai RMB/mt
- Rebar HRB400 20mm In warehouse Tianjin RMB/mt
- TMT Bar Fe500 12mm In warehouse Mumbai INR/mt
- TMT Bar Fe500 16mm In warehouse Mumbai INR/mt
### Sections (26)
- Angle Steel Q235 50*50*5mm In warehouse Guangzhou RMB/mt
- Angle Steel Q235 50*50*5mm In warehouse Shanghai RMB/mt
- Angle Steel Q235 50*50*5mm In warehouse Tianjin RMB/mt
- Channel Steel Q235 160*65*8.5mm In warehouse Guangzhou RMB/mt
- Channel Steel Q235 160*65*8.5mm In warehouse Shanghai RMB/mt
- Channel Steel Q235 160*65*8.5mm In warehouse Tianjin RMB/mt
- Dysprosium Metal 99.5%min EXW China RMB/kg
- Dysprosium Metal 99.5%min FOB China USD/kg
- Dysprosium Oxide 99.5%min EXW China RMB/kg
- Dysprosium Oxide 99.5%min FOB China USD/kg
- Erbium Oxide 99.5%min EXW China RMB/kg
- Erbium Oxide 99.5%min FOB China USD/kg
- Europium Metal 99.5%min FOB China USD/kg
- Europium Oxide 99.999%min EXW China RMB/kg
- Europium Oxide 99.999%min FOB China USD/kg
- Ferro-dysprosium 80% EXW China RMB/kg
- Ferro-dysprosium 80% FOB China USD/kg
- H-beam Steel Q235 200*200mm In warehouse Guangzhou RMB/mt
- H-beam Steel Q235 200*200mm In warehouse Shanghai RMB/mt
- H-beam Steel Q235 200*200mm In warehouse Tianjin RMB/mt
- H-beam Steel Q235 300*300mm In warehouse Guangzhou RMB/mt
- H-beam Steel Q235 300*300mm In warehouse Shanghai RMB/mt
- H-beam Steel Q235 300*300mm In warehouse Tianjin RMB/mt
- I-beam Steel Q235 250*118*10mm In warehouse Guangzhou RMB/mt
- I-beam Steel Q235 250*118*10mm In warehouse Shanghai RMB/mt
- I-beam Steel Q235 250*118*10mm In warehouse Tianjin RMB/mt
### Strip (6)
- HR Strip Q195 295*2.5mm In warehouse Guangzhou RMB/mt
- HR Strip Q195 295*2.5mm In warehouse Shanghai RMB/mt
- HR Strip Q195 295*2.5mm In warehouse Tianjin RMB/mt
- HR Strip Q195 685*2.75mm In warehouse Guangzhou RMB/mt
- HR Strip Q235B 685*5.5mm In warehouse Shanghai RMB/mt
- HR Strip Q235B 685*5.5mm In warehouse Tianjin RMB/mt
### Wire Rod (11)
- LaCe Mischmetal Ce/RE 65%min, Fe 0.3%max EXW China RMB/mt
- LaCe Mischmetal Ce/RE 65%min, Fe 1%max EXW China RMB/mt
- LaCe Mischmetal Ce/RE 65%min, Fe 1%max FOB China USD/kg
- Wire Rod JIS SWRM15 ?6.5mm In warehouse Bangkok THB/mt
- Wire Rod Q195 6.5mm FOB China USD/mt
- Wire Rod Q235 8mm In warehouse Guangzhou RMB/mt
- Wire Rod Q235 8mm In warehouse Shanghai RMB/mt
- Wire Rod Q235 8mm In warehouse Tianjin RMB/mt
- Wire Rod SAE1008 5.5mm In warehouse Kuala Lumpur MYR/mt
- Wire Rod SAE1008 5.5mm In warehouse Mumbai INR/mt
- Wire Rod SAE1008 6.5mm FOB China USD/mt
## Ferroalloys (119)
### Calcium Silicon (4)
- Calcium-silicon 28-55 EXW China RMB/mt
- Calcium-silicon 28-55 FOB China USD/mt
- Calcium-silicon 30-60 EXW China RMB/mt
- Calcium-silicon 30-60 FOB China USD/mt
### Chrome Ore (16)
- Chrome Conc. 40%min FOB S.A. USD/mt
- Chrome Conc. 42%min FOB S.A. USD/mt
- Chrome Conc. 46%min FOB Turkey USD/mt
- Chrome Conc. S.A. 40%min CIF China USD/mt
- Chrome Conc. S.A. 40%min In port China RMB/dmtu
- Chrome Conc. S.A. 42%min CIF China USD/mt
- Chrome Conc. S.A. 42%min In port China RMB/dmtu
- Chrome Conc. Turkish 46%min CIF China USD/mt
- Chrome Conc. Turkish 46%min In port China RMB/dmtu
- Chrome Conc. Zimbabwean 48%min CIF China USD/mt
- Chrome Conc. Zimbabwean 48%min In port China RMB/dmtu
- Chrome Lump 40%min FOB Turkey USD/mt
- Chrome Lump S.A. 38%min CIF China USD/mt
- Chrome Lump S.A. 38%min In port China RMB/dmtu
- Chrome Lump Turkish 40%min CIF China USD/mt
- Chrome Lump Turkish 40%min In port China RMB/dmtu
### Chromium Silicon (1)
- Silico-chrome Si 42%min, Cr 30%min EXW China RMB/mt
### Ferroboron (3)
- Ferro-boron B 18%min EXW China RMB/mt
- Ferro-boron B 18%min FOB China USD/mt
- Ferro-boron B 19%min EXW China RMB/mt
### Ferrochrome (16)
- Ferro-chrome Cr 52%min, C 8%max EXW China RMB/mt 50-basis
- Ferro-chrome Cr 55%min, C 0.1%max EXW China RMB/mt 60-basis
- Ferro-chrome Cr 55%min, C 0.25%max EXW China RMB/mt 60-basis
- Ferro-chrome Cr 60%min, C 0.1%max FOB China USD/lb Cr
- Ferro-chrome Cr 60%min, C 0.1%max In warehouse Pittsburgh USD/lb Cr
- Ferro-chrome Cr 60%min, C 0.1%max In warehouse Rotterdam USD/lb Cr
- Ferro-chrome Cr 60%min, C 8%max In warehouse Pittsburg USD/lb Cr
- Ferro-chrome Cr 60%min, C 8%max In warehouse Rotterdam USD/lb Cr
- Ferro-chrome Cr 60%min, C 8.5%max In warehouse Russia RUB/kg
- Ferro-chrome Cr 65%min, C 0.1%max In warehouse Russia RUB/kg
- Ferro-chrome Indian Cr 58%min, C 8%max CIF China USD/lb Cr
- Ferro-chrome Indian Cr 58%min, C 8%max In port China RMB/mt 50-basis
- Ferro-chrome Kazakhstan Cr 68%min, C 8.5%max CIF China USD/lb Cr
- Ferro-chrome Kazakhstan Cr 68%min, C 8.5%max In port China RMB/mt 50-basis
- Ferro-chrome S.A. Cr 50%min, C 8%max CIF China USD/lb Cr
- Ferro-chrome S.A. Cr 50%min, C 8%max In port China RMB/mt 50-basis
### Ferromanganese (8)
- Ferro-manganese Mn 65%min, C 7%max EXW China RMB/mt
- Ferro-manganese Mn 75%min, C 2%max EXW China RMB/mt
- Ferro-manganese Mn 75%min, C 8%max EXW China RMB/mt
- Ferro-manganese Mn 78%min, C 1.5%max EXW China RMB/mt
- Ferro-manganese Mn 78%min, C 7%max In warehouse Russia RUB/kg
- Ferro-manganese Mn 78%min, C 8%max Delivered Europe EUR/mt
- Ferro-manganese Mn 78%min, C 8%max In warehouse Pittsburgh USD/gt
- Ferro-manganese Mn 80%min, C 0.7%max EXW China RMB/mt
### Ferromolybdenum (8)
- Ferro-molybdenum 60%min Delivered India INR/kg
- Ferro-molybdenum 60%min EXW China RMB/mt
- Ferro-molybdenum 60%min In port South Korea USD/kg Mo
- Ferro-molybdenum 60%min In warehouse Russia RUB/kg
- Ferro-molybdenum 65%min In warehouse Rotterdam USD/kg Mo
- Ferro-molybdenum 65%min In warehouse US USD/kg Mo
- Molybdenum Oxide 57%min In port South Korea USD/lb
- Molybdenum Oxide 57%min In warehouse Rotterdam USD/lb
### Ferronickel (4)
- Ferro-nickel 1.5%min EXW China RMB/mt
- Ferro-nickel 10%min EXW China RMB/mtu
- Nickel Ore Philippine 1.5%min CIF China USD/mt
- Nickel Ore Philippine 1.8%min In port China RMB/mt
### Ferroniobium (6)
- Ferro-niobium 50-A EXW China RMB/mt
- Ferro-niobium 60-A EXW China RMB/mt
- Ferro-niobium Brazilian 66% Delivered China RMB/mt
- Ferro-niobium Brazilian 66% In warehouse Rotterdam USD/kg Nb
- Niobium Conc. Nb2O5 50%min, Ta2O5 5%min, CIF China USD/lb Oxides
- Niobium Pentoxide 99.5%min EXW China RMB/kg
### Ferrophosphorus (1)
- Ferro-phosphorus 23%min FOB China USD/mt
### Ferrosilicon (11)
- Ferro-silicon 65%min FOB Russia USD/mt
- Ferro-silicon 65%min In warehouse Russia RUB/kg
- Ferro-silicon 70%min EXW India INR/mt
- Ferro-silicon 72%min EXW China RMB/mt
- Ferro-silicon 72%min Ex-VAT FOB China USD/mt
- Ferro-silicon 72%min FOB China USD/mt
- Ferro-silicon 75%min EXW China RMB/mt
- Ferro-silicon 75%min Ex-VAT FOB China USD/mt
- Ferro-silicon 75%min FOB China USD/mt
- Ferro-silicon 75%min In warehouse Pittsburgh USD/lb Si
- Ferro-silicon 75%min In warehouse Rotterdam EUR/mt
### Ferrotitanium (8)
- Ferro-titanium 30% EXW China RMB/mt 25-basis
- Ferro-titanium 30% EXW India INR/kg
- Ferro-titanium 70% (scrap) EXW China RMB/mt
- Ferro-titanium 70% In port India INR/kg
- Ferro-titanium 70%min Delivered US USD/kg Ti
- Ferro-titanium 70%min EXW Russia RUB/kg
- Ferro-titanium 70%min FOB Russia USD/kg Ti
- Ferro-titanium 70%min In warehouse Rotterdam USD/kg Ti
### Ferrotungsten (5)
- Ferro-tungsten 70%min EXW China RMB/mt
- Ferro-tungsten 75%min EXW China RMB/mt
- Ferro-tungsten 75%min FOB China USD/kg W
- Ferro-tungsten 75%min FOB Russia USD/kg W
- Ferro-tungsten 75%min In warehouse Rotterdam USD/kg W
### Ferrovanadium (9)
- Ferro-vanadium 50%min CIF China USD/kg V
- Ferro-vanadium 50%min Delivered China RMB/mt
- Ferro-vanadium 50%min In port China RMB/mt
- Ferro-vanadium 50%min In warehouse Russia RUB/kg
- Ferro-vanadium 80%min FOB China USD/kg V
- Ferro-vanadium 80%min In warehouse Pittsburgh USD/kg V
- Ferro-vanadium 80%min In warehouse Rotterdam USD/kg V
- Vanadium Nitride N 14%min EXW China RMB/mt
- Vanadium Pentoxide Flake 98%min In warehouse Rotterdam USD/lb V2O5
### Manganese Ore (12)
- Manganese Ore 38%min FOB S.A. USD/dmtu
- Manganese Ore Australian 45%min In Qinzhou Port RMB/mtu
- Manganese Ore Australian 45%min In Tianjin Port RMB/mtu
- Manganese Ore Brazilian 44%min In Qinzhou Port RMB/mtu
- Manganese Ore Brazilian 44%min In Tianjin Port RMB/mtu
- Manganese Ore Carbonate, Mn 13%min EXW China RMB/mtu
- Manganese Ore Gabonese 44%min In Qinzhou Port RMB/mtu
- Manganese Ore Gabonese 44%min In Tianjin Port RMB/mtu
- Manganese Ore S.A. 36%min In Qinzhou Port RMB/mtu
- Manganese Ore S.A. 36%min In Tianjin Port RMB/mtu
- Manganese Ore S.A. 37%min In Qinzhou Port RMB/mtu
- Manganese Ore S.A. 37%min In Tianjin Port RMB/mtu
### Silicomanganese (7)
- Silico-manganese 60/14 EXW China RMB/mt
- Silico-manganese 60/14 FOB India USD/mt
- Silico-manganese 65/17 Delivered Europe EUR/mt
- Silico-manganese 65/17 EXW China RMB/mt
- Silico-manganese 65/17 FOB India USD/mt
- Silico-manganese 65/17 In warehouse Pittsburgh USD/lb
- Silico-manganese 65/17 In warehouse Russia RUB/kg
## Minor Metals (238)
### Antimony (12)
- Antimony Conc. 50%min Delivered China RMB/mt Sb
- Antimony Ingot 99.65%min EXW China RMB/mt
- Antimony Ingot 99.65%min In port India INR/kg
- Antimony Ingot 99.65%min In warehouse Baltimore USD/lb
- Antimony Ingot 99.65%min In warehouse Rotterdam USD/mt
- Antimony Ingot 99.85%min EXW China RMB/mt
- Antimony Trioxide 99.5%min Delivered China RMB/mt
- Antimony Trioxide 99.5%min FOB China USD/mt
- Antimony Trioxide 99.5%min In warehouse Baltimore USD/lb
- Antimony Trioxide 99.5%min In warehouse Rotterdam EUR/kg
- Antimony Trioxide 99.8%min Delivered China RMB/mt
- Ethylene Glycol Antimony 57%min Delivered China RMB/mt
### Arsenic (4)
- Arsenic Metal 99%min EXW China RMB/mt
- Arsenic Metal 99%min FOB China USD/mt
- Arsenic Trioxide 99%min EXW China RMB/mt
- Arsenic Trioxide 99%min FOB China USD/mt
### Bismuth (6)
- Bismuth Ingot 99.99%min Delivered Europe USD/lb
- Bismuth Ingot 99.99%min Delivered India INR/kg
- Bismuth Ingot 99.99%min Delivered US USD/lb
- Bismuth Ingot 99.99%min EXW China RMB/mt
- Bismuth Ingot 99.99%min FOB China USD/lb
- Bismuth Oxide 99.9%min EXW China RMB/mt
### Cadmium (4)
- Cadmium Ingot 99.99%min Delivered India INR/kg
- Cadmium Ingot 99.99%min EXW China RMB/mt
- Cadmium Ingot 99.99%min In warehouse Baltimore USD/lb
- Cadmium Ingot 99.99%min In warehouse Rotterdam USD/lb
### Calcium (6)
- Calcium Granule Electrolytic 98.5%min EXW China RMB/mt
- Calcium Granule Electrolytic 98.5%min FOB China USD/mt
- Calcium Granule Reductive 98%min EXW China RMB/mt
- Calcium Granule Reductive 98%min FOB China USD/mt
- Calcium Ingot Reductive 98%min EXW China RMB/mt
- Fluorspar 97%min Delivered China RMB/mt
### Chromium (4)
- Chromic Anhydride 99.8%min EXW China RMB/mt
- Chromium Metal 99%min EXW China RMB/mt
- Chromium Metal 99%min FOB China USD/mt
- Chromium Oxide Green 99%min EXW China RMB/mt
### Cobalt (17)
- Cobalt Carbonate 46%min Delivered China RMB/mt
- Cobalt Chloride 24%min Delivered China RMB/mt
- Cobalt Conc. 6-8% CIF China USD/lb Co
- Cobalt Intermediate 20-30% CIF China USD/lb Co
- Cobalt Metal 99.8%min Delivered China RMB/kg
- Cobalt Metal 99.8%min In warehouse Baltimore USD/lb
- Cobalt Metal 99.8%min In warehouse Rotterdam USD/lb
- Cobalt Monoxide 78%min Delivered China RMB/kg
- Cobalt Oxide 72%min Delivered China RMB/kg
- Cobalt Oxide 72%min In warehouse Rotterdam EUR/kg
- Cobalt Powder 99.5%min Delivered China RMB/kg
- Cobalt Sulfate 20.5%min Delivered China RMB/mt
- Cobalt Tetroxide 73.5%min Delivered China RMB/kg
- Lithium Cobaltate Co 60%min Delivered China RMB/kg
- NCM 523 Delivered China RMB/mt
- NCM 622 Delivered China RMB/mt
- Samarium Cobalt 30H EXW China RMB/kg
### Gallium (3)
- Gallium Metal 99.99%min EXW China RMB/kg
- Gallium Metal 99.99%min FOB China USD/kg
- Gallium Metal 99.99999%min EXW China RMB/kg
### Germanium (7)
- Germanium Dioxide 99.99%min Delivered US USD/kg
- Germanium Dioxide 99.99%min EXW China RMB/kg
- Germanium Dioxide 99.99%min FOB China USD/kg
- Germanium Metal 99.99%min Delivered Europe USD/kg
- Germanium Metal 99.99%min Delivered US USD/kg
- Germanium Metal 99.99%min EXW China RMB/kg
- Germanium Metal 99.99%min FOB China USD/kg
### Indium (4)
- Indium Ingot 99.995%min Delivered Europe USD/kg
- Indium Ingot 99.995%min Delivered US USD/kg
- Indium Ingot 99.995%min EXW China RMB/kg
- Indium Ingot 99.995%min FOB China USD/kg
### Lithium (18)
- LNCMO 523 Delivered China RMB/kg
- LNCMO 622 Delivered China RMB/kg
- Lithium Carbonate 99%min Delivered China RMB/mt
- Lithium Carbonate 99.5%min CIF China USD/kg
- Lithium Carbonate 99.5%min Delivered China RMB/mt
- Lithium Carbonate 99.5%min Delivered EU USD/kg
- Lithium Carbonate 99.5%min Delivered US USD/kg
- Lithium Carbonate 99.5%min FOB South America USD/kg
- Lithium Chloride 99.3%min Delivered China RMB/mt
- Lithium Hydroxide Monohydrate LiOH 56.5%min Delivered China RMB/mt
- Lithium Hydroxide Monohydrate LiOH 56.5%min, Magnets 0.0001%max Delivered China RMB/mt
- Lithium Hydroxide Monohydrate LiOH 56.5%min, Magnets 0.0001%max Delivered South Korea USD/kg
- Lithium Hydroxide Monohydrate LiOH 56.5%min, Magnets 0.0001%max FOB China USD/kg
- Lithium Iron Phosphate Li 3.9%min Delivered China RMB/mt
- Lithium Manganate Mn 58% Delivered China RMB/mt
- Lithium Metal 99%min Delivered China RMB/mt
- Lithium Metal 99.9%min Delivered China RMB/mt
- Spodumene Li2O 6%min CIF China USD/mt
### Magnesium (15)
- Magnesium Alloy AM50A EXW China RMB/mt
- Magnesium Alloy AM50A FOB China USD/mt
- Magnesium Alloy AM60B EXW China RMB/mt
- Magnesium Alloy AM60B FOB China USD/mt
- Magnesium Alloy AZ91D EXW China RMB/mt
- Magnesium Alloy AZ91D FOB China USD/mt
- Magnesium Ingot 99.9%min EXW China RMB/mt
- Magnesium Ingot 99.9%min Ex-VAT FOB China USD/mt
- Magnesium Ingot 99.9%min FOB China USD/mt
- Magnesium Ingot 99.9%min In port India INR/kg
- Magnesium Ingot 99.9%min In warehouse Rotterdam USD/mt
- Magnesium Ingot 99.9%min in warehouse Russia RUB/kg
- Magnesium Ingot 99.95%min EXW China RMB/mt
- Magnesium Powder 99.9%min EXW China RMB/mt
- Magnesium Powder 99.9%min FOB China USD/mt
### Manganese (11)
- Manganese Briquette 97%min FOB China USD/mt
- Manganese Dioxide Alkaline 91%min EXW China RMB/mt
- Manganese Dioxide Carbon 91%min EXW China RMB/mt
- Manganese Flake 99.7%min CIF India USD/mt
- Manganese Flake 99.7%min EXW China RMB/mt
- Manganese Flake 99.7%min Ex-VAT FOB China USD/mt
- Manganese Flake 99.7%min FOB China USD/mt
- Manganese Flake 99.7%min In warehouse Rotterdam USD/mt
- Manganese Lump 96.5%min FOB China USD/mt
- Manganese Lump 96.5%min In warehouse Russia RUB/kg
- Manganese Sulfate Mn 32%min EXW China RMB/t
### Mercury (2)
- Mercury Metal 99.9%min Ex-VAT EXW China RMB/kg
- Mercury Metal 99.999%min Ex-VAT EXW China RMB/kg
### Molybdenum (14)
- Ammonium Heptamolybdate 54%min EXW China RMB/mt
- Ammonium Tetrathiomolybdate 56%min EXW China RMB/mt
- Molybdenum Bar 99.9%min EXW China RMB/kg
- Molybdenum Conc. 45%min EXW China RMB/mtu
- Molybdenum Oxide 57%min In port India USD/lb
- Molybdenum Oxide 50%min EXW China RMB/mtu
- Molybdenum Oxide 57%min CIF China USD/lb Mo
- Molybdenum Oxide 57%min In port South Korea USD/lb
- Molybdenum Oxide 57%min In warehouse Rotterdam USD/lb
- Molybdenum Oxide 99.9%min EXW China RMB/mt
- Molybdenum Powder 99.95%min EXW China RMB/kg
- Molybdenum Scrap Cutting Scrap 99.95%min Ex-VAT EXW China RMB/kg
- Molybdenum Scrap Wire Scrap 99.95%min Ex-VAT EXW China RMB/kg
- Sodium Molybdate 99%min EXW China RMB/mt
### Niobium (5)
- Niobium Conc. Nb2O5 50%min, Ta2O5 5%min, CIF China USD/lb Oxides
- Niobium Pentoxide 99.5%min EXW China RMB/kg
- Niobium Pentoxide 99.5%min FOB China USD/kg
- Niobium Pentoxide 99.99%min EXW China RMB/kg
- Niobium Pentoxide 99.99%min FOB China USD/kg
### Rhenium (1)
- Rhenium APR 99.99%min EXW China RMB/kg
### Selenium (8)
- Selenium Dioxide 98%min Delivered China RMB/kg
- Selenium Powder 99.5%min In warehouse Rotterdam USD/lb
- Selenium Powder 99.9%min CIF China USD/lb
- Selenium Powder 99.9%min Delivered China RMB/kg
- Selenium Powder 99.9%min Delivered India INR/kg
- Selenium Powder 99.9%min Delivered US USD/lb
- Selenium Powder 99.9%min FOB Russia USD/kg
- Selenium Powder 99.95%min Delivered China RMB/kg
### Silicon (23)
- Polysilicon 9N Delivered China RMB/kg
- Silicon Metal 2-2-02 Delivered China RMB/mt
- Silicon Metal 2-2-02 Ex-VAT FOB China USD/mt
- Silicon Metal 2-2-02 FOB China USD/mt
- Silicon Metal 3-3-03 Delivered China RMB/mt
- Silicon Metal 3-3-03 Ex-VAT FOB China USD/mt
- Silicon Metal 3-3-03 FOB China USD/mt
- Silicon Metal 3-3-03 P 50ppm max Delivered China RMB/mt
- Silicon Metal 3-3-03 P 50ppm max Ex-VAT FOB China USD/mt
- Silicon Metal 3-3-03 P 50ppm max FOB China USD/mt
- Silicon Metal 4-2-1 Delivered China RMB/mt
- Silicon Metal 4-4-1 Delivered China RMB/mt
- Silicon Metal 4-4-1 Delivered Europe EUR/mt
- Silicon Metal 4-4-1 Ex-VAT FOB China USD/mt
- Silicon Metal 4-4-1 FOB China USD/mt
- Silicon Metal 4-4-1 In port India INR/kg
- Silicon Metal 5-5-3 Delivered China RMB/mt
- Silicon Metal 5-5-3 Delivered Europe EUR/mt
- Silicon Metal 5-5-3 Ex-VAT FOB China USD/mt
- Silicon Metal 5-5-3 FOB China USD/mt
- Silicon Metal 5-5-3 In port India INR/kg
- Silicon Metal 98.5%min Delivered US USD/lb
- Silicon Powder 99%min Delivered China RMB/mt
### Strontium (2)
- Strontium Metal 99%min EXW China RMB/mt
- Strontium Metal 99%min FOB China USD/mt
### Tantalum (9)
- Tantalum Conc. Ta2O5 30%min CIF China USD/lb Ta2O5
- Tantalum Conc. Ta2O5 30%min in warehouse Rotterdam USD/lb Ta2O5
- Tantalum Metal 99.95%min Delivered US USD/kg
- Tantalum Metal 99.95%min EXW China RMB/kg
- Tantalum Metal 99.95%min FOB China USD/kg
- Tantalum Pentoxide 99.5%min EXW China RMB/kg
- Tantalum Pentoxide 99.5%min FOB China USD/kg
- Tantalum Pentoxide 99.99%min EXW China RMB/kg
- Tantalum Pentoxide 99.99%min FOB China USD/kg
### Tellurium (3)
- Tellurium Metal 99.9%min In warehouse Rotterdam USD/kg
- Tellurium Metal 99.99%min EXW China RMB/kg
- Tellurium Metal 99.99%min In warehouse Rotterdam USD/kg
### Titanium (18)
- Ilmenite Conc Australian TiO2 55-58%min, Fe 27%min CIF China USD/mt
- Ilmenite Conc Kenyan TiO2 48%min, Fe2O3 20%max CIF China USD/mt
- Ilmenite Conc Mozambican TiO2 52%min, Fe2O3 25%max CIF China USD/mt
- Ilmenite Conc TiO2 46%min; Fe2O3 8%max; Ex-VAT EXW China RMB/mt
- Ilmenite Conc TiO2 50%min; Fe 30%min; In port China RMB/mt
- Ilmenite Conc TiO2 50%min; Fe2O3 14%max; In port China RMB/mt
- Ilmenite Conc TiO2 52%min, Fe2O3 25%max FOB Vietnam USD/mt
- Reduced Ilmenite TiO2 56%min EXW China RMB/mt
- Rutile Conc TiO2 95%min In port China RMB/mt
- Titanium Dioxide A TiO2 98%min Delivered China RMB/mt
- Titanium Dioxide R TiO2 93%min Delivered China RMB/mt
- Titanium Dioxide R TiO2 93%min FOB China USD/mt
- Titanium Pipe TA2 25.4x1mm EXW China RMB/kg
- Titanium Plate TA2 2mm EXW China RMB/kg
- Titanium Slag TiO2 74%min EXW China RMB/mt
- Titanium Slag TiO2 90%min EXW China RMB/mt
- Titanium Sponge 99.7%min EXW China RMB/mt
- Titanium Tetrachloride 99.99%min EXW China RMB/mt
### Tungsten (18)
- Sodium Tungstate WO3 68%min EXW China RMB/mt
- Thorium Tungsten Electrode ThO2 1.7-2.2% EXW China RMB/kg
- Tungsten APT 88.5%min EXW China RMB/mt
- Tungsten APT 88.5%min FOB China USD/mtu
- Tungsten APT 88.5%min In warehouse Rotterdam USD/mtu
- Tungsten Bar W-4 99.9%min EXW China RMB/kg
- Tungsten Bar W-4 99.9%min FOB China USD/kg
- Tungsten Carbide 99.8%min 2.5-7.0?m EXW China RMB/kg
- Tungsten Carbide 99.8%min 2.5-7.0?m FOB China USD/kg
- Tungsten Ore WO3 50%min FOB Africa USD/mtu
- Tungsten Ore WO3 65%min EXW China RMB/mt
- Tungsten Oxide WO3 99.95%min EXW China RMB/mt
- Tungsten Oxide WO3 99.95%min FOB China USD/mtu
- Tungsten Powder 99.95%min 2.5-7.0?m EXW China RMB/kg
- Tungsten Scrap Anvil 90%min Ex-VAT Delivered China RMB/kg
- Tungsten Scrap CNC Cutting Blades 70%min Ex-VAT Delivered China RMB/kg
- Tungsten Scrap Drills 70%min Ex-VAT Delivered China RMB/kg
- Tungsten Scrap Grinding Swarf W 70%min, Co 5%min Ex-VAT Delivered China RMB/mtu
### Vanadium (11)
- Ammonium Metavanadate 98%min EXW China RMB/mt
- Vanadium Nitride N 14%min EXW China RMB/mt
- Vanadium Pentoxide Flake 92%min CIF China USD/lb V2O5
- Vanadium Pentoxide Flake 92%min In port China RMB/mt
- Vanadium Pentoxide Flake 98%min CIF China USD/lb V2O5
- Vanadium Pentoxide Flake 98%min EXW China RMB/mt
- Vanadium Pentoxide Flake 98%min In port China RMB/mt
- Vanadium Pentoxide Flake 98%min In warehouse Rotterdam USD/lb V2O5
- Vanadium Pentoxide Powder 98%min EXW China RMB/mt
- Vanadium Pentoxide Powder 99%min EXW China RMB/mt
- Vanadium Pentoxide Powder 99.5%min EXW China RMB/mt
### Zirconium (13)
- Fused Zirconia Zr(Hf)O2 98.5%min EXW China RMB/mt
- Zircon Sand Australian Zr(Hf)O2 66%min In port China RMB/mt
- Zircon Sand Australian Zr(Hf)O2 66%min In port China USD/mt
- Zircon Sand Indonesian Zr(Hf)O2 66%min CIF China USD/mt
- Zircon Sand S.A. Zr(Hf)O2 65%min In port China RMB/mt
- Zircon Sand S.A. Zr(Hf)O2 65%min In port China USD/mt
- Zircon Sand S.A. Zr(Hf)O2 66%min In port China USD/mt
- Zircon Sand Zr(Hf)O2 65%min EXW China RMB/mt
- Zirconium Carbonate Zr(Hf)O2 40%min EXW China RMB/mt
- Zirconium Carbonate Zr(Hf)O2 40%min, Cl 50ppm EXW China RMB/mt
- Zirconium Oxychloride Zr(Hf)O2 36%min EXW China RMB/mt
- Zirconium Silicate Zr(Hf)O2 64.5%min EXW China RMB/mt
- Zirconium Sponge Zr+Hf 99.4%min EXW China RMB/kg
## Rare Earths (54)
### Lutetium (1)
- Lutetium Oxide 99.99%min China RMB/kg
### Neodymium (27)
- Cerium Carbonate TREO 45%min, CeO2/REO 100% EXW China RMB/mt
- Cerium Carbonate TREO 45%min, CeO2/REO 100% FOB China USD/mt
- Cerium Metal 99%min EXW China RMB/mt
- Cerium Metal 99%min FOB China USD/kg
- Cerium Oxide 99%min FOB China USD/mt
- Cerium Oxide 99.9%min EXW China RMB/mt
- Cerium Oxide 99.9%min In warehouse Rotterdam USD/kg
- Cerium Oxide 99.99%min EXW China RMB/mt
- LaCe Carbonate TREO 45%min, CeO2/REO 65±2% EXW China RMB/mt
- LaCe Chloride TREO 45%min, CeO2/REO 65±2% EXW China RMB/mt
- NdFeB Sintered Rough 35H EXW China RMB/kg
- NdFeB Sintered Rough 35M EXW China RMB/kg
- NdFeB Sintered Rough 45H EXW China RMB/kg
- NdFeB Sintered Rough 45M EXW China RMB/kg
- NdFeB Sintered Rough 48H EXW China RMB/kg
- NdFeB Sintered Rough 50H EXW China RMB/kg
- NdFeB Sintered Rough 50M EXW China RMB/kg
- NdFeB Sintered Rough N35 EXW China RMB/kg
- NdFeB Sintered Rough N45 EXW China RMB/kg
- NdFeB Sintered Rough N52 EXW China RMB/kg
- Neodymium Metal 99%min EXW China RMB/mt
- Neodymium Metal 99%min FOB China USD/kg
- Neodymium Oxide 99.5%min EXW China RMB/mt
- Neodymium Oxide 99.5%min FOB China USD/mt
- PrNd Mischmetal Pr 25%, Nd 75% EXW China RMB/mt
- PrNd Mischmetal Pr 25%, Nd 75% FOB China USD/kg
- PrNd Oxide Pr6O11 25%, Nd2O3 75% EXW China RMB/mt
### Praseodymium (4)
- Praseodymium Metal 99.5%min FOB China USD/kg
- Praseodymium Oxide 99.5%min EXW China RMB/mt
- Praseodymium Oxide 99.5%min FOB China USD/mt
- Praseodymium Oxide 99.5%min In warehouse Rotterdam USD/kg
### Samarium (6)
- Samarium Cobalt 30H EXW China RMB/kg
- Samarium Metal 99.5%min EXW China RMB/kg
- Samarium Metal 99.5%min FOB China USD/kg
- Samarium Oxide 99.9%min EXW China RMB/mt
- Samarium Oxide 99.9%min FOB China USD/mt
- SmEuGd Oxide TREO 220g/L min, Eu2O3/REO 8%min EXW China RMB/mt
### Scandium (4)
- Scandium Aluminum Sc 2% EXW China RMB/kg
- Scandium Metal 99.99%min EXW China RMB/kg
- Scandium Metal 99.999%min EXW China RMB/kg
- Scandium Oxide 99.99%min EXW China RMB/kg
### Terbium (4)
- Terbium Metal 99.9%min EXW China RMB/kg
- Terbium Metal 99.9%min FOB China USD/kg
- Terbium Oxide 99.99%min EXW China RMB/kg
- Terbium Oxide 99.99%min FOB China USD/kg
### Ytterbium (1)
- Ytterbium Oxide 99.99%min EXW China RMB/kg
### Yttrium (7)
- YEu Oxide Eu2O3/REO 6.6%min EXW China RMB/kg
- YEu Oxide Eu2O3/REO 6.6%min FOB China USD/kg
- Yttrium Metal 99.9%min EXW China RMB/kg
- Yttrium Metal 99.9%min FOB China USD/kg
- Yttrium Oxide 99.999%min EXW China RMB/mt
- Yttrium Oxide 99.999%min FOB China USD/kg
- Yttrium Oxide 99.999%min In warehouse Rotterdam USD/kg
## Refractories (77)
### Calcined Bauxite (3)
- Calcined Bauxite 85%min 1-3mm EXW China RMB/mt
- Calcined Bauxite 86%min 1-3mm FOB China USD/mt
- Calcined Bauxite 87%min 1-3mm EXW China RMB/mt
### Carbon (15)
- Calcined Pet Coke S 3%max EXW China RMB/mt
- Coal Tar Pitch Coking Value 53%min EXW China RMB/mt
- Graphite Electrode H.P. D300mm FOB China USD/mt
- Graphite Electrode H.P. D400mm EXW China RMB/mt
- Graphite Electrode H.P. D450mm FOB China USD/mt
- Graphite Electrode H.P. D500mm EXW China RMB/mt
- Graphite Electrode R.P. D400mm EXW China RMB/mt
- Graphite Electrode U.H.P. D500mm EXW China RMB/mt
- Graphite Electrode U.H.P. D500mm FOB China USD/mt
- Graphite Electrode U.H.P. D600mm EXW China RMB/mt
- Graphite Electrode U.H.P. D600mm FOB China USD/mt
- Pet Coke S 2%max EXW China RMB/mt
- Pet Coke S 3%max EXW China RMB/mt
- Prebaked Anode S 3%max EXW China RMB/mt
- Prebaked Anode S 3%max FOB China USD/mt
### Fused Alumina (16)
- Calcined Alumina 99%min 1-3mm US USD/st
- Fused Alumina Brown 95%min 1-3mm EXW China RMB/mt
- Fused Alumina Brown 95%min 1-3mm EXW India INR/mt
- Fused Alumina Brown 95%min 1-3mm FOB China USD/mt
- Fused Alumina Brown 95%min 1-3mm In warehouse Rotterdam EUR/mt
- Fused Alumina Brown 95%min 1-3mm US USD/st
- Fused Alumina Brown 95%min F36 EXW China RMB/mt
- Fused Alumina Brown 95%min F36 EXW India INR/mt
- Fused Alumina Brown 95%min F36 FOB China USD/mt
- Fused Alumina White 99%min 1-3mm EXW China RMB/mt
- Fused Alumina White 99%min 1-3mm FOB China USD/mt
- Fused Alumina White 99%min 1-3mm In warehouse Rotterdam EUR/mt
- Fused Alumina White 99%min 1-3mm US USD/st
- Fused Alumina White 99%min F36 EXW China RMB/mt
- Fused Alumina White 99%min F36 FOB China USD/mt
- Tabular Alumina 99%min 1-3mm US USD/st
### Graphite (11)
- Amorphous Graphite 75%min 0-3mm EXW China RMB/mt
- Amorphous Graphite 80%min 20-50mm EXW China RMB/mt
- Amorphous Graphite 80%min 200mesh min EXW China RMB/mt
- Expansible Graphite 99 80 250 EXW China RMB/mt
- Flake Graphite +195 EXW China RMB/mt
- Flake Graphite +895 EXW China RMB/mt
- Flake Graphite -190 EXW China RMB/mt
- Flake Graphite -194 EXW China RMB/mt
- Flake Graphite -194 FOB China USD/mt
- Flake Graphite -199 EXW China RMB/mt
- Spherical Graphite 99.95%min 17?m max EXW China RMB/mt
### Magnesia (21)
- Magnesia C.C. 85%min 200mesh EXW China RMB/mt
- Magnesia C.C. 90%min 200mesh EXW China RMB/mt
- Magnesia C.C. 90%min 200mesh FOB China USD/mt
- Magnesia C.C. 97%min 0-5mm EXW China RMB/mt
- Magnesia D.B. 90%min 3-15mm EXW China RMB/mt
- Magnesia D.B. 90%min 3-15mm FOB China USD/mt
- Magnesia D.B. 90%min 3-15mm In warehouse Rotterdam EUR/mt
- Magnesia D.B. 90%min 3-15mm US USD/st
- Magnesia D.B. 95%min 0-30mm EXW China RMB/mt
- Magnesia D.B. 95%min 0-30mm FOB China USD/mt
- Magnesia D.B. 95%min 0-30mm In warehouse Rotterdam EUR/mt
- Magnesia D.B. 96.5%min 0-30mm EXW China RMB/mt
- Magnesia D.B. 97%min 0-30mm EXW China RMB/mt
- Magnesia D.B. 97%min 0-30mm FOB China USD/mt
- Magnesia E.F. 97%min EXW China RMB/mt
- Magnesia E.F. 97%min H-Ca EXW China RMB/mt
- Magnesia E.F. 97%min H-Ca FOB China USD/mt
- Magnesia E.F. 97.5%min Large Crystal EXW China RMB/mt
- Magnesia E.F. 97.5%min Large Crystal FOB China USD/mt
- Magnesite Lump 47%min EXW China RMB/mt
- Magnesite Pellet 47%min EXW China RMB/mt
### Silicon Carbide (11)
- Silicon Carbide Black 88%min 0-10mm EXW China RMB/mt
- Silicon Carbide Black 88%min 0-10mm FOB China USD/mt
- Silicon Carbide Black 98%min 10-30cm EXW China RMB/mt
- Silicon Carbide Black 98%min F16-100 EXW China RMB/mt
- Silicon Carbide Black 98%min F16-100 FOB China USD/mt
- Silicon Carbide Green 98%min F240 EXW China RMB/mt
- Silicon Carbide Green 98%min F240 FOB China USD/mt
- Silicon Carbide Green 98%min JIS2000 EXW China RMB/mt
- Silicon Carbide Green 98%min JIS2000 FOB China USD/mt
- Silicon Carbide Green 98.5%min 10-30cm EXW China RMB/mt
- Silicon Carbide Black 88%min 0-10mm In warehouse Rotterdam EUR/mt
## Stainless & Special (16)
### Bearing Steel (10)
- Cold Heading Wire 22A ?6.5mm In warehouse Shanghai RMB/mt
- Electrical Steel Grain Oriented 130 0.3*980mm In warehouse China RMB/mt
- Electrical Steel Non-grain 470 0.5*1200mm In warehouse China RMB/mt
- Electrical Steel Non-grain 800 0.5*1,200mm In warehouse China RMB/mt
- Stainless Bar 321 60mm In warehouse Dainan RMB/mt
- Stainless CR Coil 201/2B 1.0mm In warehouse China RMB/mt
- Stainless CR Coil 304/2B 1.0mm In warehouse China RMB/mt
- Stainless CR Coil 430/2B 1.0mm In warehouse China RMB/mt
- Stainless HR Coil 201/No.1 6.0mm In warehouse China RMB/mt
- Stainless HR Coil 304/No.1 6.0mm In warehouse China RMB/mt
### Stainless Bar (2)
- Stainless Bar 304 60mm In warehouse Dainan RMB/mt
- Stainless Bar 321 60mm In warehouse Dainan RMB/mt
### Stainless Pipe (1)
- Stainless Seamless Pipe 304 108*4mm In warehouse Wenzhou RMB/mt
### Stainless Scrap (2)
- Stainless Scrap 304 Solid In warehouse Tokyo JPY/mt
- Stainless Scrap 304 Solid In warehouse Dainan RMB/mt
### Structural Steel (1)
- Stainless CR Coil 304/2B 1.0mm In warehouse China RMB/mt
## Steel Raw Materials (28)
### Coal (5)
- Coking Coal A 10%max, S 0.8%max EXW Hebei RMB/mt
- Coking Coal A 10%max, S 0.8%max EXW Henan RMB/mt
- Coking Coal A 10.5%max, S 0.8%max EXW Shanxi RMB/mt
- Thermal Coal Q5000 In port Qinhuangdao RMB/mt
- Thermal Coal Q5500 In port Qinhuangdao RMB/mt
### Coke (3)
- Met. Coke A 13%max, S 0.7%max EXW Hebei RMB/mt
- Met. Coke A 13%max, S 0.7%max EXW Shandong RMB/mt
- Met. Coke A 13%max, S 0.7%max EXW Shanxi RMB/mt
### Iron (3)
- Cast Iron Z18-22 EXW China RMB/mt
- Pig Iron L8-10 EXW China RMB/mt
- S.G. Cast Iron Q10-12 EXW China RMB/mt
### Iron Ore (9)
- Iron Ore Fine 65%min EXW Benxi RMB/dt
- Iron Ore Fine 65%min EXW Laiwu RMB/dt
- Iron Ore Fine 65%min Ex-VAT EXW Daixian RMB/wt
- Iron Ore Fine 66%min Ex-VAT EXW Handan RMB/dt
- Iron Ore Fine Australian 58%min In port China RMB/wt
- Iron Ore Fine Australian 62%min CNF China USD/dt
- Iron Ore Fine Australian 62%min In port China RMB/wt
- Iron Ore Fine Brazilian 65%min In port China RMB/wt
- Iron Ore Lump Australian 62%min In port China RMB/wt
### Steel Billet (2)
- Steel Billet HRB335 150x150mm EXW China RMB/mt
- Steel Billet Q235 150x150mm EXW China RMB/mt
### Steel Scrap (6)
- Steel Scrap HMS 4-6mm EXW Shandong RMB/mt
- Steel Scrap HMS 8mm min EXW Wuxi RMB/mt
- Steel Scrap MMS 4-6mm EXW Guangzhou RMB/mt
- Steel Scrap MMS 4-6mm EXW Tianjin RMB/mt
- Steel Scrap MMS 6-10mm EXW Liaoning RMB/mt
- Steel Scrap Process Scrap Delivered China RMB/mt

34
PRODUCT.md Normal file
View File

@ -0,0 +1,34 @@
# Product
## Register
product
## Users
Didvan is used by market analysts, steel/metals operators, administrators, and trusted partner systems that need current market data. Admin users manage access, data-source tokens, health checks, reports, and operational visibility.
## Product Purpose
The product collects, stores, and serves market data for metals, steel, domestic Iranian markets, commodities, crypto, steel-company stocks, macro indicators, and reports. Success means the dashboard and partner API expose current data reliably, with clear access control and enough operational visibility to know when feeds are healthy.
## Brand Personality
Calm, exact, operational. The interface should feel like a serious market-intelligence tool: dense enough for work, readable under repeated use, and restrained rather than promotional.
## Anti-references
Avoid marketing-page composition, decorative cards, oversized hero treatments, vague SaaS copy, and visual effects that distract from data quality, security, or admin workflows.
## Design Principles
- Make operational state visible: users should quickly see what is fresh, broken, authorized, or stale.
- Prefer explicit data tables and compact controls over decorative layouts.
- Treat security as a visible workflow: tokens, access, and usage should be inspectable without exposing secrets.
- Keep Persian/RTL admin workflows readable while preserving exact technical values such as paths, tokens, and timestamps.
- Optimize for repeated work: low-friction scanning, filtering, and verification matter more than spectacle.
## Accessibility & Inclusion
Use clear contrast, readable table density, keyboard-accessible controls, and reduced-motion-safe UI. Preserve UTF-8 Persian text correctly and keep technical identifiers legible in monospace where helpful.

222
README.md Normal file
View File

@ -0,0 +1,222 @@
<div dir="rtl">
# سامانهٔ جامع آمار و اطلاعات «دیدوان»
داشبورد زندهٔ بازار فلزات، فولاد، ارز، طلا/سکه، کامودیتی و کریپتو — به‌همراه آرشیو گزارش‌های منابع معتبر جهانی. این سند برای **توسعه‌دهندهٔ بعدی** نوشته شده؛ کل معماری، نحوهٔ کار، راه‌اندازی، امنیت و دام‌ها (gotchas) را پوشش می‌دهد. کامل بخوان.
---
## ۱) این سیستم چیست؟
یک اپلیکیشن سه‌بخشی:
| بخش | تکنولوژی | پورت | نقش |
|---|---|---|---|
| **فرانت‌اند** | TanStack Start + React 19 + Vite 7 + Tailwind v4 | `5174` | لندینگ، ورود/ثبت‌نام، داشبورد |
| **بک‌اند (API)** | FastAPI + SQLite + Playwright | `8000` | اسکرپرها، API دادهٔ بازار، احراز هویت |
| **پنل ادمین** | FastAPI (HTML درون‌خطی) | `8001` | بازبینی/تأیید کاربران، مدیریت توکن، سلامت سرویس‌ها، گزارش‌ها |
دیتای بازار با **اسکرپرهای پس‌زمینه** جمع می‌شود و در **SQLite** ذخیره می‌شود (فقط مقادیرِ تغییرکرده — dedup). فرانت‌اند هر چند ثانیه از API می‌خواند و «زنده» نشان می‌دهد.
---
## ۲) ساختار پوشه‌ها
```
asianmetal-dashboard/
├─ src/ # فرانت‌اند
│ ├─ routes/ # مسیرهای فایل‌محورِ TanStack
│ │ ├─ index.tsx # لندینگ (LaserFlow، پنل زنده، گالری‌ها، …)
│ │ ├─ login.tsx # ورود (کاراکترهای کارتونی + احراز واقعی)
│ │ ├─ register.tsx # ثبت‌نام (→ pending)
│ │ ├─ dashboard.tsx # داشبورد + همهٔ تب‌ها/کارت‌ها/چارت‌ها
│ │ └─ __root.tsx # لایهٔ ریشه (RTL، فونت Vazirmatn، لود styles)
│ ├─ components/ # LaserFlow, CircularGallery, CardSwap, TubelightNavbar, …
│ ├─ components/ui/ # shadcn/ui
│ ├─ lib/ # labels.ts، metals-data.ts، utils.ts
│ ├─ styles.css # توکن‌های تم (oklch)، .tile/.glass/.panel، انیمیشن‌ها
│ └─ routeTree.gen.ts # خودکار توسط TanStack (دستی ویرایش نکن)
├─ backend/ # API + اسکرپرها
│ ├─ main.py # اپ FastAPI، CORS/هدرها/ریت‌لیمیت، همهٔ اندپوینت‌ها، lifespan
│ ├─ db.py # اتصال SQLite + init_db (ساخت/مهاجرت جداول)
│ ├─ auth.py # هشِ رمز (PBKDF2) + نشست‌های سمت‌سرور
│ ├─ currency_stream.py # اسکرپِ ارز/طلا/سکهٔ داخلی (Playwright، هر ۶۰ث)
│ ├─ crypto_market.py # کریپتو از API رایگانِ CoinGecko (هر ۶۰ث)
│ ├─ world_economy.py # شاخص‌های کلانِ اقتصادهای بزرگ (هفتگی)
│ ├─ steel_stocks.py # سهام شرکت‌های فولادی (companiesmarketcap، هر ۵ دقیقه)
│ ├─ live_stream.py # کامودیتی لحظه‌ای (Yahoo Finance) روی WebSocket
│ ├─ reports.py # اسکنِ آرشیو PDFِ گزارش‌ها (فایل‌سیستم)
│ └─ sync.py # اسکریپت دستیِ سینکِ قیمت فلزات از API منبعِ بالادست
├─ admin/admin_app.py # پنل ادمین (مستقل)
├─ vite.config.ts # پورت ۵۱۷۴ + proxy (/api,/ws → :8000) + allowedHosts
├─ .gitignore # شاملِ .env، __pycache__، *.db
└─ README.md # همین فایل
```
---
## ۳) راه‌اندازی محلی
**پیش‌نیازها:** Node 20+ (یا Bun)، Python 3.11+، و برای اسکرپرها `playwright install chromium`.
```powershell
# نصب وابستگی‌های فرانت
npm install
# نصب وابستگی‌های پایتون (نمونه)
pip install fastapi uvicorn playwright requests yfinance pandas websockets
python -m playwright install chromium
```
### اجرای هر سه سرویس (هرکدام در ترمینال جدا)
```powershell
# ۱) بک‌اند (پورت 8000)
$env:PYTHONUTF8="1"; python -m uvicorn main:app --host 127.0.0.1 --port 8000 --app-dir backend
# ۲) فرانت‌اند (پورت 5174)
npm run dev
# ۳) پنل ادمین (پورت 8001) — ADMIN_PASSWORD الزامی است
$env:ADMIN_USER="admin"; $env:ADMIN_PASSWORD="یک‌رمزِ‌قوی"; python -m uvicorn admin_app:app --host 127.0.0.1 --port 8001 --app-dir admin
```
سپس: فرانت روی `http://localhost:5174` ، پنل ادمین روی `http://localhost:8001`.
---
## ۴) متغیرهای محیطی
| متغیر | کجا | پیش‌فرض | توضیح |
|---|---|---|---|
| `DB_PATH` | backend, admin | مسیر محلی | محل فایل SQLite (هر دو سرویس باید یکی باشند) |
| `ASIANMETAL_TOKEN` | sync.py | — | توکنِ API بالادست (یا از پنل ادمین در DB) — **هیچ‌وقت در کد هاردکد نکن** |
| `ADMIN_USER` / `ADMIN_PASSWORD` | admin | `admin` / **الزامی** | احراز Basic پنل ادمین؛ بدون `ADMIN_PASSWORD` بالا نمی‌آید (fail-closed) |
| `ALLOWED_ORIGINS` | backend | `localhost:5174,127.0.0.1:5174` | originهای مجاز CORS (با کاما) |
| `COOKIE_SECURE` | backend | `0` | در پروداکشن (HTTPS) روی `1` بگذار تا کوکیِ نشست Secure شود |
| `REPORTS_DIR` | reports.py | پوشهٔ آرشیو | محل PDFهای گزارش (ساختار `<منبع> - <دسته>/<file>.pdf`) |
---
## ۵) دیتابیس (SQLite)
`init_db()` در `db.py` جداول را می‌سازد و **مهاجرتِ امن** انجام می‌دهد (ستون‌ها را اگر نبودند `ALTER` می‌کند):
- **`prices`** — تاریخچهٔ روزانهٔ قیمت فلزات: `grp, category, title, date, low, mid, high` (یکتا روی `title,date`). منبعِ تب «فلزات و فولاد».
- **`users`** — کاربران: `id, name, email, phone, organization, password_hash, status('pending'|'approved'|'rejected'), created_at`.
- **`sessions`** — نشست‌های سمت‌سرور: `token, user_id, created_at, expires_at`.
- **`app_config`** — جفت‌های `key/value` (مثلاً `asianmetal_token`، هارت‌بیت اسکرپرها).
- جداول اسکرپرها: `currency_prices, gold_prices, coin_prices`, `te_prices`, `world_economy`, `steel_stocks` — هرکدام آخرین/تاریخچهٔ مقادیر را نگه می‌دارند (dedup روی تغییر).
> نکته: قیمت‌های لحظه‌ای (ارز/کریپتو/کامودیتی) عمدتاً در **حافظه** کش می‌شوند (`_latest`) و فقط تغییرها در DB ذخیره می‌شوند تا حجم کم بماند.
---
## ۶) اسکرپرها (در `lifespan` بک‌اند خودکار استارت می‌شوند)
| ماژول | منبع | بازه | جدول/خروجی |
|---|---|---|---|
| `currency_stream` | نرخ داخلیِ ارز/طلا/سکه (URL در همان فایل) | ۶۰ ثانیه | `currency_prices`/`gold_prices`/`coin_prices` |
| `crypto_market` | **CoinGecko** (API رایگان، بدون کلید) | ۶۰ ثانیه | حافظه (`get_latest`) |
| `world_economy` | شاخص‌های کلانِ اقتصادهای بزرگ | هفتگی | `world_economy` |
| `steel_stocks` | companiesmarketcap.com | ۵ دقیقه | `steel_stocks` |
| `live_stream` | **Yahoo Finance** | لحظه‌ای | WebSocket `/ws/live` |
| `reports` | فایل‌سیستمِ `REPORTS_DIR` | کشِ ۱۰ دقیقه | اسکنِ PDF |
**دامِ مهم:** سایت‌های منبع قیمت‌ها را با JS لِیزی‌لود می‌کنند. اسکرپرهای Playwright بعد از `goto` یک `wait_for_timeout(50009000)` دارند؛ **حذفش نکن** وگرنه مقادیرِ کهنه/خالی خوانده می‌شود. هر اسکرپر هم try/except دارد تا یک خطای گذرا کلِ تسک پس‌زمینه را نکُشد.
`sync.py` یک اسکریپتِ **دستی** است (در lifespan نیست) که قیمت فلزات را از API بالادست می‌گیرد؛ توکن را از `app_config` یا env `ASIANMETAL_TOKEN` می‌خواند.
---
## ۷) اندپوینت‌های API (بک‌اند `:8000`)
**عمومی (دادهٔ بازار):** `GET /api/tree`, `/api/history?title=&days=`, `/api/search?q=`, `/api/latest`, `/api/live`, `/api/currency`, `/api/gold`, `/api/coin`, `/api/world-economy`, `/api/steel-stocks`, `/api/crypto`, `/api/reports`, `/api/reports/files?folder=`, `/api/reports/file?folder=&name=` (استریمِ inline PDF با گاردِ traversal)، WebSocket `/ws/live`.
**احراز هویت:** `POST /api/register`، `POST /api/login`، `GET /api/me`، `POST /api/logout`.
**پنل ادمین `:8001`** (پشت HTTP Basic): `/api/users`، `POST /api/users/{id}/approve|reject`، `GET|POST /api/token`، `/api/health`، `/api/test-scrape`، `/api/reports`، `/api/export?dataset=`.
---
## ۸) احراز هویت و گردشِ تأیید (مهم)
> اصل: **هیچ اعتمادی به کلاینت نیست.** ورود کاملاً سمت‌سرور تأیید می‌شود؛ نشست در **کوکیِ httpOnly** است، نه `localStorage`.
```
ثبت‌نام (نام، نام‌خانوادگی، سازمان، ایمیل، موبایل، رمز)
│ POST /api/register → رمز با PBKDF2 هش می‌شود، رکورد با status='pending'
کاربر در وضعیت «در انتظار تأیید» ──► ادمین در پنل (تبِ کاربران) تأیید/رد می‌کند
│ (POST /api/users/{id}/approve | reject)
ورود POST /api/login → اگر approved بود: کوکیِ نشست (httpOnly, SameSite=Lax) ست می‌شود
│ pending → 403 «در انتظار تأیید» ، rejected → 403 ، رمز غلط → 401
داشبورد → گاردِ سمت‌سرور: fetch('/api/me')؛ اگر 401 شد → ری‌دایرکت به /login
خروج POST /api/logout → نشست از DB حذف + کوکی پاک می‌شود
```
نکات پیاده‌سازی:
- هشِ رمز: `auth.hash_password` با `hashlib.pbkdf2_hmac('sha256', ..., 200000)` — بدونِ وابستگیِ خارجی. مقایسه با `hmac.compare_digest`.
- توکنِ نشست: `secrets.token_urlsafe(32)`، انقضای ۷ روزه، در جدول `sessions`.
- ریت‌لیمیتِ per-IP (در حافظه) روی `register`/`login` (۵ در دقیقه).
- اعتبارسنجیِ Pydantic: فرمت/طولِ ایمیل و موبایل، حداقل ۸ کاراکترِ رمز.
- رد کردنِ کاربر، نشست‌های فعالش را هم پاک می‌کند (دسترسی فوراً قطع).
---
## ۹) پنل ادمین (`:8001`)
- ورود با HTTP Basic؛ `ADMIN_PASSWORD` **الزامی** است (بدونش بالا نمی‌آید).
- تب‌ها: **کاربران** (لیست + تأیید/رد + سازمان/وضعیت)، **توکن منبع داده** (ذخیره/تستِ توکن بالادست)، **سلامت سرویس‌ها** (هارت‌بیتِ هر اسکرپر + تست اسکرپ)، **گزارشات** (دیتاست‌ها + خروجی CSV).
- خروجیِ کاربران در UI با `esc()` فرار داده می‌شود (جلوگیری از XSSِ ذخیره‌شده).
---
## ۱۰) فرانت‌اند
- مسیرها: `/` (لندینگ)، `/login`، `/register`، `/dashboard`.
- داشبورد، تب‌ها (`VIEW_NAV`): **بازار داخلی**، **کامودیتی**، **فلزات و فولاد** (کاتالوگِ کارت + چارت)، **اقتصاد جهانی** (جدول حرارتی)، **سهام فولادی**، **کریپتو**، **گزارش‌ها**.
- زبان بصری: تم **لایتِ کرم + لهجهٔ کهربایی (فولاد مذاب)**؛ توکن‌ها در `styles.css` (oklch). قیمت‌کارت‌ها سبکِ **Spark-canvas** (نمودار پس‌زمینه + رقم‌های غلتانِ `Odometer` + پیلِ درصد).
- API نسبی است (`const API = ""`) تا از طریق **پراکسیِ Vite** هم‌مبدأ کار کند → برای share فقط یک تونل لازم است.
- RTL سراسری، فونت **Vazirmatn**. اعداد همه با همین فونت (نه مونو).
---
## ۱۱) اشتراک‌گذاری (ngrok)
چون `vite.config.ts` مسیرهای `/api` و `/ws` را به `:8000` پراکسی می‌کند و فرانت از URLهای نسبی استفاده می‌کند، **فقط یک تونل روی ۵۱۷۴ کافی است**:
```powershell
ngrok http 5174
# یا مستقیم باینری: & "$env:APPDATA\npm\node_modules\ngrok\bin\ngrok.exe" http 5174
```
نکته: شیمِ `ngrok.ps1`ی npm گاهی پورت را اشتباه پاس می‌دهد؛ اگر تونل به پورت غلط رفت، **مستقیم `ngrok.exe`** را صدا بزن. URL را از `http://127.0.0.1:4040/api/tunnels` هم می‌توانی بخوانی. (پلنِ رایگان: هر بار URL عوض می‌شود و یک صفحهٔ هشدارِ اولیه دارد.)
---
## ۱۲) امنیت — وضعیت و کارهای واجب
سخت‌سازی‌های انجام‌شده: CORS محدود، هدرهای امنیتی (`nosniff`, `X-Frame-Options`, `Referrer-Policy`)، اعتبارسنجی + ریت‌لیمیتِ ورودی‌ها، محدودیتِ کوئریِ `history`/`search`، گاردِ path-traversal روی استریمِ PDF، کوئری‌های پارامتری، احراز هویتِ سمت‌سرور با کوکیِ httpOnly، و حذفِ توکنِ هاردکد از کد.
**واجب قبل از پروداکشن:**
1. توکنِ بالادست (SteelStatista) که قبلاً در git history لو رفته را **باطل/تمدید** کن؛ مقدارِ جدید فقط در پنل ادمین/`env`.
2. `ADMIN_PASSWORD` قوی بگذار (پیش‌فرضِ ضعیف حذف شده).
3. در HTTPS واقعی: `COOKIE_SECURE=1` و `ALLOWED_ORIGINS` را به دامنهٔ واقعی محدود کن.
4. پنل ادمین (`:8001`) را **عمومی نکن** (در تونل نگذار؛ فقط لوکال/VPN/SSH).
---
## ۱۳) دام‌ها و بدهیِ فنی (برای دفعهٔ بعد)
- `DB_PATH`/مسیرها در `db.py` و `admin_app.py` پیش‌فرضِ ویندوزیِ مطلق دارند (شاملِ نام کاربری) — بهتره به مسیر نسبی/`env` منتقل شود.
- وابستگی‌های `package.json` با `^` پین نشده‌اند؛ برای بیلدِ تکرارپذیر `npm ci` + پین.
- فایل‌های `__pycache__/*.pyc` قبلاً کامیت شده‌اند؛ untrack کن: `git rm -r --cached backend/__pycache__`.
- `routeTree.gen.ts` خودکار است — دستی ویرایش نکن (با اضافه‌شدن فایل route، خودِ Vite بازتولید می‌کند).
- اسکرپرها به ساختار HTMLِ منابع وابسته‌اند؛ اگر منبع قالبش را عوض کند، سلکتورها/`wait_for_timeout` باید تنظیم شوند.
</div>

706
SECURITY-CHECKLIST.md Normal file
View File

@ -0,0 +1,706 @@
# 🔒 The Comprehensive Security Checklist for Vibe-Coded Apps & AI Agents
> A merged, expanded checklist built from the **vibe-check** ruleset (17 categories) and **Sherlock Forensics' 7 prompts**, plus the things both of them miss: **anti-scraping**, **API-abuse prevention**, **AI/LLM-agent security**, **infrastructure hardening**, and **monitoring**.
>
> Goal: make it genuinely hard for a random person — or a bored kid with a script — to break in, scrape your data, or run up your bill.
---
## ⚠️ Read this first (honest reality check)
There is **no such thing as "unhackable."** Anyone who tells you otherwise is selling something. What you *can* do is raise the cost of attacking you so high that casual attackers move on and serious ones are slowed down and detected. That's the realistic goal: **defense in depth**, not invincibility.
This checklist gets you past the mistakes that have *actually* taken down real vibe-coded apps in production — none of which required a sophisticated attack. When you have real users and real data, **hire a human penetration tester**. No checklist replaces someone actively trying to break your stuff.
Two stats worth remembering:
- Carnegie Mellon found AI-generated code is ~61% functionally correct but only ~10.5% *secure*. The gap between "it works" and "it's safe" is where you get breached.
- Broken access control (someone reading another user's data by changing an ID) has been the #1 web vulnerability for years. It's also the easiest to introduce and the easiest to exploit.
---
## How to use this checklist (3 ways)
This document does three jobs at once. Use all three.
**1. As rules your AI coding agent reads while it writes (prevention).**
Copy the **[Agent Rules](#-agent-rules-paste-into-claudemd--agentsmd)** section at the bottom into your project root as `CLAUDE.md` (Claude Code) or `AGENTS.md` (Cursor, Copilot, Codex, Windsurf, Gemini CLI). Commit it. Your AI reads it automatically from then on and stops creating these vulnerabilities in the first place.
**2. As an audit your AI agent runs against existing code (detection).**
Paste the prompts in **[Copy-Paste AI Audit Prompts](#-copy-paste-ai-audit-prompts)** into your AI tool, one at a time. Tell it to investigate, report what it found, fix it, and verify. Don't batch them — one category at a time, fully, or it skims.
**3. As manual tests you run yourself (verification).**
Work through **[Manual Penetration Tests](#-manual-penetration-tests-do-these-yourself)**. These catch the things AI can't verify from inside the code — like whether your live database is actually queryable from the open internet right now.
> **If your product itself is an AI agent** (a chatbot, an LLM-powered assistant, an autonomous tool-using agent), pay special attention to **[Section 7: AI / LLM Agent Security](#7--ai--llm-agent-security-the-part-everyone-forgets)**. That's the attack surface that doesn't exist in normal apps and that neither source covers.
---
## 🚨 TIER 0 — The five that actually take companies down
If you do nothing else today, do these five. Every documented vibe-coded breach traces back to one of them. None required a clever attack.
- [ ] **Database is not publicly queryable.** Row Level Security (RLS) enabled on *every* table, default-deny, policies scoped to the logged-in user. Test it with a raw `curl` using only your public/anon key (see Manual Test #1).
- [ ] **Every protected API route rejects unauthenticated requests** with a 401, *before* the handler runs — not checked inside the function where it's easy to forget.
- [ ] **No secrets in your git repo.** `.env` is in `.gitignore` *before the first commit*. No API keys, DB passwords, or tokens anywhere in source. Scan git history, not just current files.
- [ ] **Users can't access each other's data by changing an ID.** Every route that takes a resource ID verifies `current_user owns that resource` — a *separate* check from "are you logged in."
- [ ] **No secret keys in the browser.** Open DevTools → Sources → search for `sk_`, `AKIA`, `secret`, `private_key`. Secret keys live server-side only; the frontend gets *publishable* keys at most.
Everything below makes you more secure. These five keep you out of the headlines.
---
## 1. 🔑 Secrets & Credentials
The fastest way to get owned: ship a secret key to the browser or commit it to GitHub. Bots scan public repos for leaked keys within *minutes* of a push.
- [ ] `.env` is listed in `.gitignore` **before** the first commit is ever made.
- [ ] `git ls-files .env` returns nothing (the file is not tracked).
- [ ] No API keys, DB credentials, or tokens are hardcoded in any source file.
- [ ] No secret lives in a client-exposed env var: anything prefixed `NEXT_PUBLIC_`, `VITE_`, or `REACT_APP_` is **bundled into the browser** — only publishable keys belong there.
- [ ] `.env.example` exists with **placeholder values only**, never real credentials.
- [ ] Git *history* has been scanned for previously-committed secrets (use `gitleaks detect --source . --verbose`). A deleted secret still lives in history.
- [ ] **Any key that was ever committed has been rotated.** Once it touches a public repo, treat it as compromised — even if you deleted it seconds later.
- [ ] Secrets are loaded server-side only and injected via your host's secret manager in production (Vercel/Netlify/Railway env vars, AWS Secrets Manager, Doppler, etc.), not shipped in a file.
**Fix pattern:** Move every secret to a server-only environment variable. Proxy any third-party call that needs a secret key through your own backend route so the key never reaches the client.
---
## 2. 🗄️ Database Security
Your database is the prize. If the anon/public key can read tables directly, you have no security at all — the rest of the app is decoration.
- [ ] **Row Level Security (RLS) is enabled on EVERY table** before deployment (Supabase). No exceptions, including "internal" tables.
- [ ] Every table has **explicit policies scoped to the user** (`auth.uid()`), with a default of **deny all**.
- [ ] No policy uses `USING (true)` or `FOR ALL` without a real `WHERE` condition. That's an "allow everyone everything" policy in disguise.
- [ ] **Firebase rules require `request.auth != null`** and scope access to `request.auth.uid`. The default open ruleset is wide open to the planet.
- [ ] A `curl` to your REST endpoint with **only the public/anon key** returns empty or 403 for tables that hold user data (see Manual Test #1).
- [ ] Service-role / admin database keys are **never** exposed to the client and never used in frontend code.
- [ ] **Never deserialize untrusted data.** No `pickle.loads`/`pickle.load` (Python) or equivalent on anything a user can influence. Use JSON for all network data.
- [ ] Database backups are enabled and **the restore process has actually been tested** (a backup you've never restored is a hope, not a backup).
- [ ] The DB user your app connects with has **least privilege** — it can't `DROP TABLE` or read tables it doesn't need.
---
## 3. 👤 Authentication
Authentication is the front door. AI builds login flows that *look* complete but skip the security requirements.
- [ ] Passwords are hashed with **bcrypt, Argon2, or scrypt** (cost factor ≥ 10). **Never** MD5, SHA-1, or plain SHA-256 — those are crackable in bulk if your DB leaks.
- [ ] Login, registration, and password-reset endpoints are **rate limited** (see Section 14).
- [ ] **JWTs / session tokens expire** (≤ 24 hours for access tokens; use short-lived access + refresh tokens for longer sessions).
- [ ] Session tokens are **actually invalidated on logout** — server-side, not just deleted from the browser.
- [ ] **Password reset links expire** within ~1 hour and are single-use.
- [ ] Session cookies are set with `httpOnly: true`, `secure: true`, and `sameSite: 'lax'` (or `strict`).
- [ ] **Multi-factor authentication (MFA/2FA)** is available, and required for admin accounts.
- [ ] **Account enumeration is prevented:** login, signup, and password-reset return the *same* message and timing whether or not the email exists ("If that account exists, we sent a link"). Don't tell attackers which emails are registered.
- [ ] **New passwords are checked against breach lists** (e.g. Have I Been Pwned's k-anonymity API) and a minimum length is enforced (length beats complexity rules).
- [ ] Email/identity is verified before granting full access where it matters.
- [ ] After N failed logins, the account or IP is throttled or temporarily locked (with care not to enable a lockout-as-DoS).
> Using a managed auth provider (Supabase Auth, Auth0, Clerk, Firebase Auth)? Most of the hashing/session mechanics are handled for you — but you still own MFA config, enumeration behavior, session expiry settings, and authorization (Section 4).
---
## 4. 🛂 Authorization & Access Control (IDOR)
**This is the big one.** Authentication asks "who are you?" Authorization asks "are you allowed to touch *this specific thing*?" AI almost always builds the first and forgets the second. Changing `/api/orders/123` to `/api/orders/124` and getting someone else's order is the single most common — and most damaging — vibe-coded vulnerability.
- [ ] **Every route that takes a resource ID verifies ownership:** `current_user.id == resource.owner_id`. This is a check you write, separate from authentication.
- [ ] The ownership check exists on **reads (GET)** *and* **writes (PUT/PATCH/DELETE)** — not just writes. Reading someone else's data is still a breach.
- [ ] Failing the ownership check returns **403** (not 404, not the data).
- [ ] **Vertical privilege escalation is blocked:** a regular user hitting an admin endpoint gets 403. Admin status is checked server-side on every admin route.
- [ ] **Mass assignment / over-posting is prevented:** a user can't set fields like `role: "admin"`, `is_verified: true`, `account_balance: 9999`, or `user_id: <someone_else>` just by adding them to the request body. Whitelist the fields you accept; never blindly spread the request body into your DB model.
- [ ] IDs that don't *need* to be guessable use **UUIDs**, not sequential integers (defense in depth — UUIDs don't replace ownership checks, they just make enumeration harder).
- [ ] Authorization is enforced at the **API/server layer**, never relying on the frontend hiding a button. Hidden ≠ protected.
**Fix pattern:** In every handler, after you know who the user is, fetch the resource and explicitly compare ownership before returning or modifying it. Centralize this so it's hard to forget.
---
## 5. 🔌 API Security & Abuse Prevention
> *You specifically asked about people misusing your API. Neither source covers this in depth — here's the dedicated section.*
An exposed API is an open invitation to automate abuse: scraping every record, hammering expensive endpoints, brute-forcing, or running up your metered bills (OpenAI, Twilio, SMS, etc.). Auth alone doesn't stop a *legitimate* user from abusing the API.
**Access & authentication**
- [ ] Every non-public endpoint requires authentication (API key, OAuth token, or session) — verified server-side before the handler.
- [ ] API keys are **scoped** (least privilege — read-only keys exist, keys are tied to specific resources/permissions) and **revocable** instantly.
- [ ] Keys can be **rotated** without downtime, and you have a tested procedure to kill a leaked key.
- [ ] Keys are hashed at rest in your DB (store a hash, show the raw key only once at creation).
**Quotas, throttling & cost control**
- [ ] **Per-key / per-user rate limits** exist on *all* endpoints, not just login. Tier them (e.g. free vs paid).
- [ ] **Hard usage quotas** per key per day/month, so one key can't drain your whole budget.
- [ ] **Spend caps / budget alerts** on every metered downstream service (OpenAI, Anthropic, Twilio, SendGrid, cloud egress). Set provider-side billing limits *and* your own circuit breaker that stops calling out when a threshold is hit.
- [ ] **Request size limits** (max body size, max upload size) enforced server-side to prevent memory-exhaustion DoS.
- [ ] **Result-set caps:** list/search endpoints have a maximum page size and require pagination. No endpoint returns "all rows" — that's a scraping endpoint waiting to happen.
**Anti-enumeration & data-volume protection**
- [ ] Endpoints that return lists **cap how much can be pulled** over a time window (a user pulling 50,000 records in an hour is exfiltration, not usage).
- [ ] Sequential/guessable IDs in API responses are avoided where they enable bulk enumeration.
- [ ] **GraphQL specifically:** query depth limiting, query cost/complexity analysis, disable introspection in production, and cap aliases/batching (otherwise one request can ask for everything).
**Monitoring**
- [ ] Per-key usage is logged and **anomalies trigger alerts** (sudden 100× spike, access from new geography, hitting endpoints in an automated pattern).
- [ ] Abusive keys can be **auto-throttled or suspended** when they cross thresholds.
**Fix pattern:** Put a rate-limiter + quota layer in front of the API (e.g. Upstash/Redis token bucket, or your gateway's built-in limits). Add a kill-switch env flag that disables expensive downstream calls if spend spikes.
---
## 6. 🕷️ Anti-Scraping & Bot Protection
> *You specifically asked about someone scraping your entire website. Neither source covers this — here's the dedicated section.*
A motivated kid with a Python script and `requests` can copy your whole site in an afternoon if nothing stops them. You can't make scraping *impossible* (anything a browser can render, a determined bot can read), but you can make it slow, expensive, and detectable so casual scrapers give up.
**Front-line bot management**
- [ ] Put the site behind a provider with **bot management / WAF** (Cloudflare, AWS WAF, Vercel's protections, Fastly). This is the single highest-leverage move — turn on bot-fight mode and managed challenge rules.
- [ ] **Rate limit by IP and by user**, site-wide, not just on auth. Burst limits + sustained limits.
- [ ] **Challenge suspicious traffic** with CAPTCHA / Turnstile / hCaptcha on high-value or high-volume flows (signup, search, bulk-data pages, password reset).
- [ ] **Block or challenge known datacenter / cloud IP ranges and bad ASNs** for sensitive endpoints — most scraping comes from AWS/GCP/Azure/OVH IPs, not residential ones.
**Make bulk extraction expensive**
- [ ] **No endpoint dumps everything.** Paginate, cap page size, and rate-limit pagination so walking all pages takes a long, throttled time.
- [ ] Sensitive data (full contact lists, pricing, proprietary content) requires auth and is metered per account.
- [ ] Consider **honeypot links/fields** invisible to humans but followed by naive crawlers — hitting them flags the client as a bot.
- [ ] Optionally **watermark or canary** your data (unique seeded records) so you can identify who leaked/scraped it later.
**Hygiene & detection**
- [ ] `robots.txt` is configured (won't stop bad actors, but stops well-behaved crawlers and is good hygiene). Don't rely on it for security.
- [ ] **Detect automation signals:** missing/!suspicious `User-Agent`, no JS execution, headless-browser fingerprints, impossible click speeds, identical timing patterns. Feed these into your bot rules.
- [ ] **Monitor for scraping patterns:** one client touching thousands of distinct resource pages, sequential ID walking, or high request volume with no normal browsing behavior → alert and throttle.
- [ ] DDoS protection is enabled at the edge (your CDN/WAF), so a flood can't take you offline or rack up egress costs.
**Reality check:** Public content that's readable in a browser is ultimately scrapable. The goal is to stop the *casual* "scrape the whole site in one script" attempt and to gate/meter the data that actually matters behind auth + per-account limits.
---
## 7. 🤖 AI / LLM Agent Security (the part everyone forgets)
> *If your product is itself an AI agent — a chatbot, an LLM assistant, a tool-using autonomous agent — this is your unique attack surface. Neither source touches it. This is where "my AI agent gets abused" actually happens.*
An LLM-powered feature introduces risks that don't exist in normal apps: users can talk the model into ignoring your rules, leaking data, calling tools it shouldn't, or burning your token budget.
**Prompt injection (the #1 LLM risk)**
- [ ] **Treat all model output as untrusted.** Never `eval()` it, never run it as SQL/shell, never auto-execute it without validation. The model can be tricked into emitting malicious instructions.
- [ ] **Direct prompt injection:** assume users *will* try "ignore previous instructions." Keep the real authorization/business logic in *your code*, not in the prompt. The system prompt is not a security boundary — server-side checks are.
- [ ] **Indirect prompt injection:** if the agent reads external content (web pages, uploaded files, emails, RAG documents), that content can contain hidden instructions. Sandbox it, strip/escape it, and don't let retrieved text silently change the agent's behavior or trigger tool calls.
- [ ] System prompt and developer instructions are **not relied on as secrets** — assume users can extract them.
**Tool / function-calling guardrails (for agents that take actions)**
- [ ] The agent operates under **least privilege** — it can only call the specific tools and access the specific data it needs, scoped to the *current user*. It must not be able to read other users' data via a tool.
- [ ] **Dangerous actions require confirmation or are off-limits** — sending money, deleting data, emailing arbitrary addresses, running code. Add human-in-the-loop for high-impact actions.
- [ ] Tool inputs the model generates are **validated like any other user input** before execution (the model is effectively an untrusted user here).
- [ ] If the agent **executes code**, it runs in a locked-down sandbox (no network, no filesystem, no secrets, resource/time limits) — never on your main server with your env vars in scope.
- [ ] The agent can't be used as an **SSRF proxy** (fetching internal URLs on the attacker's behalf — see Section 10) or as a **free passthrough to the raw LLM** (your endpoint shouldn't just relay arbitrary prompts to the paid model with no scoping).
**Cost & abuse control (this is how the "bored kid" runs up your bill)**
- [ ] **Per-user token/request quotas and rate limits** on every agent endpoint. One user must not be able to send thousands of requests and drain your OpenAI/Anthropic budget.
- [ ] **Max input length and max output tokens** enforced per request.
- [ ] **Hard spend cap + alerting** on your LLM provider account, plus a server-side circuit breaker that stops calling the model when a daily threshold is hit.
- [ ] Conversation/context length is bounded so a user can't force ever-growing (and ever-more-expensive) context windows.
**Content, privacy & output safety**
- [ ] **Input and output moderation** (provider moderation endpoint or your own filter) to stop the agent generating or amplifying harmful content that gets attributed to you.
- [ ] The agent **doesn't leak secrets, system internals, other users' data, or PII** in its responses. Don't put secrets or cross-user data in the context window in the first place.
- [ ] **Log agent inputs, tool calls, and outputs** (scrubbing PII) so you can audit abuse and debug incidents.
- [ ] Outputs that get rendered in the browser are **escaped/sanitized** — an LLM can be induced to produce `<script>` or markdown that leads to XSS (see Section 9).
- [ ] If you fine-tune or use RAG on user data, you've confirmed **one user's data can't surface in another user's responses.**
**Fix pattern:** Put a thin trusted layer around the model — authenticate the user, enforce quotas, scope tools and data to that user, validate everything the model emits before acting on it, and cap spend. The model is a powerful but untrusted component; your code stays in charge of permissions and money.
---
## 8. 💉 Input Validation & Injection
User input is hostile until proven otherwise. Client-side validation is for UX only — **all validation must happen server-side.**
- [ ] **All user input is validated server-side** (type, length, format, range). Client-side checks don't count.
- [ ] **SQL injection:** every query uses **parameterized queries / prepared statements / ORM methods**. Zero string concatenation, f-strings, `.format()`, or template literals building SQL with user input.
- [ ] **NoSQL injection** (MongoDB etc.): user input isn't passed as raw query objects; operators like `$where`/`$gt` can't be injected via request params.
- [ ] **Command injection:** user input never reaches `exec`, `system`, `subprocess`, `child_process`, `eval` unsanitized. Avoid shelling out with user data; if unavoidable, use arg arrays, never string interpolation.
- [ ] **Path traversal:** file paths built from user input are validated/normalized so `../../etc/passwd` can't escape the intended directory.
- [ ] **Open redirect:** redirect targets from user input are validated against an allowlist — don't blindly `redirect(req.query.next)`.
- [ ] **SSTI (server-side template injection):** user input is never rendered *as* a template; it's passed as *data* to the template engine.
- [ ] **XXE:** XML parsers have external-entity resolution disabled.
- [ ] **ReDoS:** user input isn't fed to catastrophic-backtracking regexes; complex regexes are bounded/tested.
- [ ] **Request body limits + JSON depth limits** prevent giant or deeply-nested payloads from exhausting memory.
---
## 9. 🧬 Output Encoding & XSS
Cross-site scripting lets an attacker run JavaScript in your users' browsers — stealing sessions, keystrokes, and data.
- [ ] **No `dangerouslySetInnerHTML`, `v-html`, or `innerHTML`** with user-supplied content unless it's first sanitized with **DOMPurify** (or an equivalent allowlist sanitizer).
- [ ] Where raw HTML rendering is genuinely required, it goes through DOMPurify every time.
- [ ] **Server-side template auto-escaping is enabled** (the default in most engines — don't disable it).
- [ ] User input reflected into HTML, attributes, JS, or URLs is **context-appropriately encoded.**
- [ ] A **Content-Security-Policy** is set (Section 13) as a second line of defense — even if XSS slips through, CSP limits what injected script can do.
- [ ] **LLM-generated content** rendered to users is treated as untrusted and sanitized too (see Section 7).
- [ ] Test it: submit `<script>alert('XSS')</script>` and `<img src=x onerror=alert('XSS')>` into every field. It should render as plain text, never fire (Manual Test #12).
---
## 10. 🌐 SSRF (Server-Side Request Forgery)
If your app fetches URLs the user supplies (link previews, image proxies, "import from URL," webhook testers), an attacker can point it at *internal* addresses — including cloud metadata endpoints that hand out your credentials.
- [ ] **All private/internal IP ranges are blocked:** `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16` (cloud metadata!), `::1`.
- [ ] **Only `http` and `https` schemes are allowed** (no `file://`, `gopher://`, `ftp://`, etc.).
- [ ] The hostname is **resolved and the resulting IP is checked *before* the request is made** — and re-checked to prevent DNS-rebinding (don't validate, then fetch a different IP).
- [ ] Redirects are not blindly followed to internal addresses.
- [ ] This applies to your **AI agent** too — if it can fetch URLs, it inherits all of these requirements (Section 7).
- [ ] If your app fetches no user-supplied URLs, mark **N/A** and note why.
Test it: submit `http://169.254.169.254/latest/meta-data/`, `http://127.0.0.1/`, `http://[::1]/`. All must be rejected before any request fires (Manual Test #6).
---
## 11. 🎭 CSRF (Cross-Site Request Forgery)
CSRF tricks a logged-in user's browser into making state-changing requests they didn't intend (e.g. a hidden form on a malicious site that changes their email on *your* site).
- [ ] Session cookies use **`SameSite=Lax` or `Strict`** (this alone blocks most CSRF), **OR**
- [ ] All state-changing endpoints (POST/PUT/PATCH/DELETE) validate a **CSRF token**.
- [ ] A cross-origin form POST to any state-changing endpoint **fails** (Manual Test #7).
- [ ] State-changing actions are never exposed via GET requests.
---
## 12. 🔀 CORS
Misconfigured CORS lets malicious websites make authenticated requests to your API using your users' credentials.
- [ ] CORS origin is an **explicit allowlist** of your actual domains — **never `*`**.
- [ ] The origin is **not dynamically reflected** back from the request header (that's a wildcard in disguise).
- [ ] **`credentials: true` is never combined with a wildcard origin** (the browser blocks it, but the intent is the bug).
- [ ] Test it: a request with `Origin: https://evil.com` should not get an `Access-Control-Allow-Origin` echoing it back (Manual Test #9).
---
## 13. 🛡️ Security Headers
Free, browser-enforced protection that AI tools almost never set. Apply them via **one global middleware**, not per-route.
- [ ] **`Content-Security-Policy`** — start with `default-src 'self'` and loosen as needed. Your strongest defense against XSS.
- [ ] **`Strict-Transport-Security: max-age=31536000; includeSubDomains`** — forces HTTPS.
- [ ] **`X-Frame-Options: DENY`** — stops clickjacking (your site embedded in a malicious iframe).
- [ ] **`X-Content-Type-Options: nosniff`** — stops MIME-sniffing attacks.
- [ ] **`Referrer-Policy: strict-origin-when-cross-origin`** — limits leaked referrer data.
- [ ] All five are present on **every** response (Express: use **helmet**; Next.js: set them in `next.config.js`).
- [ ] Verify with `curl -I https://yourapp.com` or at **securityheaders.com** (Manual Test #8).
---
## 14. 🚦 Rate Limiting & DoS Protection
Without rate limits, attackers brute-force logins, scrape data, and exhaust your resources for free.
- [ ] **Login, registration, and password-reset** endpoints are rate limited (recommend ~10 attempts per 15 min per IP; return **429** when exceeded).
- [ ] **Expensive endpoints** (search, exports, anything calling a paid downstream service or your LLM) are rate limited.
- [ ] **All API endpoints** have a sensible default limit (Section 5).
- [ ] **`X-Forwarded-For` is not trusted** for rate-limiting unless you're behind a known, trusted reverse proxy — otherwise attackers spoof it to bypass limits.
- [ ] Edge-level **DDoS protection** is enabled (Cloudflare/WAF/CDN).
- [ ] Rate limiting is enforced **server-side / at the gateway**, not in client code.
- [ ] Test it: 50 rapid failed logins should start returning 429 (Manual Test #10).
---
## 15. 📎 File Uploads
Uploads are a classic foothold: a "JPG" that's actually a script, executed on your origin.
- [ ] File type is validated by **magic bytes (actual file content)**, not the filename extension.
- [ ] Uploads are **renamed to UUIDs server-side** (no user-controlled filenames, no path traversal via filename).
- [ ] Files are stored on a **separate domain/bucket** (S3, R2, GCS) — **never** served from your app's origin, so an uploaded file can't run as same-origin script.
- [ ] **Size limits are enforced server-side** (not just a frontend attribute).
- [ ] Images are re-encoded/processed server-side where feasible (strips embedded payloads and EXIF).
- [ ] Uploaded files are **scanned for malware** where the risk warrants it.
- [ ] Test it: upload a `.jpg` containing `<script>alert('XSS')</script>` and visit its URL — it must not execute on your origin (Manual Test #14).
- [ ] No uploads? Mark **N/A**.
---
## 16. 💳 Payments & Webhooks (Stripe)
A webhook endpoint that trusts whatever hits it can be told "payment succeeded" by anyone with `curl`.
- [ ] Stripe webhook signature is **verified on every request** with `stripe.Webhook.constructEvent` (or your language's equivalent). Invalid/missing signature → **400**.
- [ ] Webhook handlers are **idempotent** — processed event IDs are stored and duplicates are skipped (Stripe retries; double-processing = double-granting).
- [ ] The **full event lifecycle** is handled, not just success: `payment_intent.succeeded`, `invoice.payment_failed`, `customer.subscription.deleted`, `customer.subscription.past_due`.
- [ ] Entitlements (what a user gets) are driven by **verified webhook events / server-side state**, never by a client-side "payment worked" redirect.
- [ ] Amounts and prices are **set/validated server-side** — the client can't submit its own price.
- [ ] Test it: POST a fake event with no signature — it must return 400, not 200 (Manual Test #13).
- [ ] No payments? Mark **N/A**.
---
## 17. 🤐 Error Handling & Information Disclosure
Verbose errors hand attackers a map: your DB schema, file paths, framework versions, and internal logic.
- [ ] A **global error handler** catches all unhandled exceptions.
- [ ] Production responses return **generic messages only** (`{"error": "Something went wrong"}`) — no stack traces, SQL errors, file paths, or library names.
- [ ] Full error detail goes to **server-side logs only**.
- [ ] **Debug/development mode is OFF in production** (Django `DEBUG=False`, Flask debug off, framework dev pages disabled).
- [ ] **Source maps are not deployed** to production (they expose your original source).
- [ ] Custom **404 / 403 / 500** pages are configured (no framework default error pages).
- [ ] Errors return **consistent JSON + appropriate HTTP status codes**.
- [ ] Test it: send invalid JSON, non-existent IDs, a lone `'` — responses must stay generic (Manual Test #15).
---
## 18. 📦 Dependencies & Supply Chain
AI tools **hallucinate package names**. Attackers pre-register those exact fake names with malware ("slopsquatting"). Installing one gives an attacker code execution on your server.
- [ ] **Every dependency the AI suggested is verified to actually exist** on the official registry (npm/PyPI/Packagist) with real download history and a maintained repo — *before* you install it.
- [ ] Package names are checked for **typosquatting** (`reqeusts` vs `requests`, `lodahs` vs `lodash`).
- [ ] **Red flags investigated:** < ~1,000 weekly downloads, published in the last 30 days, single unnamed maintainer, name suspiciously close to a popular package.
- [ ] **Exact versions are pinned** in production (no `^` or `~`).
- [ ] **Lock files are committed** (`package-lock.json`, `poetry.lock`, `yarn.lock`).
- [ ] `npm audit` / `pip-audit` shows **no critical or high** vulnerabilities (run it in CI).
- [ ] Unused/unnecessary dependencies are removed (smaller attack surface; some things are built-in language features).
- [ ] Automated dependency scanning is enabled (Dependabot / Renovate / Snyk).
---
## 19. 🏗️ Infrastructure, Deployment & Secrets Management
The app can be perfect and still fall to a misconfigured server or an over-privileged cloud key.
- [ ] **HTTPS/TLS everywhere**, valid cert, HTTP → HTTPS redirect, HSTS on (Section 13).
- [ ] Production secrets come from a **secrets manager / host env vars**, not a file in the repo (Section 1).
- [ ] **Separate environments** for dev / staging / production with **separate credentials** — no shared keys, no prod data in dev.
- [ ] Cloud **IAM follows least privilege** — services and keys can only touch what they need. No `AdministratorAccess` on app roles.
- [ ] **Storage buckets are private by default** (the classic "public S3 bucket" leak); public access is explicit and intentional.
- [ ] Admin panels, databases, and internal dashboards are **not exposed to the public internet** (VPN / IP allowlist / auth).
- [ ] Default credentials on **everything** (databases, admin tools, services) are changed.
- [ ] Server OS and runtime are **patched / on supported versions**.
- [ ] CI/CD secrets are stored in the platform's secret store, and CI logs don't print them.
- [ ] If you use containers, base images are scanned and kept updated.
---
## 20. 📊 Logging, Monitoring & Incident Response
You can't respond to what you can't see. Most breaches are detected late — or by the customer.
- [ ] **Security events are logged:** failed logins, authz failures (403s), rate-limit hits, admin actions, key creation/revocation.
- [ ] Logs **never contain secrets, passwords, full tokens, or unmasked PII**.
- [ ] **Alerts fire on anomalies:** spikes in errors/429s/403s, spend spikes on metered services, logins from new geographies, bulk data access (Sections 5, 6).
- [ ] Logs are **centralized and retained** (you can reconstruct what happened after an incident).
- [ ] You have a basic **incident-response plan**: how to revoke/rotate every key, take the app offline, notify users, and preserve evidence.
- [ ] **Everything can be rotated quickly** — DB credentials, API keys, signing secrets, OAuth secrets. Know how *before* you need to.
- [ ] A `security.txt` / responsible-disclosure contact exists so researchers can report issues to you instead of dumping them publicly.
---
## 21. 🔐 Data Protection & Privacy
- [ ] Data is **encrypted in transit** (TLS) and **at rest** (DB/storage-level encryption).
- [ ] **PII is minimized** — you collect and retain only what you actually need.
- [ ] Sensitive data has a **retention + secure-deletion policy** (and deletion actually deletes, including from backups over time).
- [ ] Users can **export and delete their data** where required (GDPR/CCPA), and you honor it.
- [ ] **Audit logs** record access to sensitive records.
- [ ] Third parties you send data to are vetted; you're not leaking PII to analytics/LLM providers unintentionally.
- [ ] Backups are **encrypted** and access-controlled (a leaked backup is a full breach).
---
# 📋 Copy-Paste AI Audit Prompts
Paste these into your AI coding agent **one at a time**. After each, tell it to: *investigate every relevant file, report what's vulnerable with severity, fix it, then verify the fix.* Don't batch — one category fully, or it skims.
> **Kickoff prompt:**
> *"Run a security audit on this project. Go through each of the prompts I give you one at a time. For each: investigate thoroughly (every file, route, config, migration, env file, frontend), write a report of what's vulnerable / safe / missing with severity (CRITICAL/HIGH/MEDIUM/LOW/PASS), make a fix plan, implement it, and verify against concrete goals. Put results in a `security/` folder. Do not move on until the current category is fully done."*
### Prompt — Secrets exposure
```
Scan the entire codebase and git history for hardcoded secrets, API keys, and credentials. Find: strings matching key patterns (sk_, pk_, key_, token_, secret_, AKIA...), DB connection strings with embedded passwords, private keys/certs in code, .env files tracked by git, and any secret living in a client-exposed env var (NEXT_PUBLIC_*, VITE_*, REACT_APP_*). Confirm .env is in .gitignore and `git ls-files .env` is empty. For each finding: file, line, and the secure fix using a server-side env var. Flag any committed secret as needing rotation.
```
### Prompt — Database access (RLS)
```
Audit database access control. For Supabase: list every table and confirm Row Level Security is enabled with explicit policies scoped to auth.uid() and a default-deny stance; flag any policy using USING(true) or FOR ALL without a WHERE condition. For Firebase: confirm rules require request.auth != null and scope to request.auth.uid. Confirm no service-role/admin DB key is used in frontend code. Show me which tables a request with only the anon/public key could read, and the fix for each.
```
### Prompt — Authentication
```
Review authentication for vulnerabilities. Check: password hashing uses bcrypt/argon2/scrypt (cost >= 10) and never MD5/SHA-1/SHA-256; rate limiting exists on login/register/password-reset; JWT/session tokens expire (<= 24h access) and are invalidated server-side on logout; password reset links expire within 1h and are single-use; session cookies set httpOnly+secure+sameSite; MFA is available and required for admins; account enumeration is prevented (identical response/timing for existing vs non-existing emails on login/signup/reset). List each issue with severity and the exact code fix.
```
### Prompt — Authorization / access control (IDOR)
```
Audit every API endpoint for authorization flaws (this is the highest-risk category). For each endpoint that takes a resource ID (in path, query, or body), confirm the handler verifies the authenticated user owns that resource (current_user.id == resource.owner_id) as a SEPARATE check from authentication, on BOTH reads and writes, returning 403 on failure. Check for horizontal escalation (User A reading User B's data by changing an ID) and vertical escalation (regular user hitting admin endpoints). Check for mass-assignment/over-posting (user setting fields like role, is_admin, balance, or user_id via the request body). List every endpoint, its current authorization status, and the fix.
```
### Prompt — API abuse & rate limiting
```
Review the API for abuse resistance. For every endpoint check: is there per-user/per-key rate limiting and a usage quota; are there hard caps on result-set size with required pagination (no endpoint returns all rows); are request body sizes limited; can sequential IDs be enumerated to scrape data; for any metered downstream service (LLM, SMS, email, cloud), is there a spend cap and circuit breaker? Confirm X-Forwarded-For isn't trusted for rate limiting unless behind a trusted proxy. For GraphQL: check query depth/cost limits and disabled introspection in production. List gaps and fixes.
```
### Prompt — AI / LLM agent security (if applicable)
```
If this project exposes an LLM/AI agent, audit it for AI-specific risks. Check: model output is never executed (no eval/SQL/shell) without validation; business logic and authorization live in code, not in the prompt; external/retrieved content (RAG, web, files, uploads) is treated as untrusted to prevent indirect prompt injection; tool/function calls are scoped to the current user with least privilege and dangerous actions require confirmation; any code execution is sandboxed with no secrets/network; per-user token and request quotas + max input/output length + an LLM spend cap and circuit breaker exist; input/output moderation is in place; the agent can't leak secrets, system prompts, PII, or other users' data; agent outputs rendered in the browser are sanitized. List each gap with severity and fix.
```
### Prompt — Injection (SQL / command / path / etc.)
```
Audit all database queries and any use of system/exec/eval for injection. Find every query built with string concatenation, f-strings, .format(), or template literals containing user input and convert to parameterized queries/prepared statements. Check for NoSQL injection (Mongo operator injection), ORM raw-query misuse, command injection (user input reaching exec/subprocess/child_process), path traversal in file operations, open redirects, SSTI, and XXE. Show vulnerable code and the fixed version side by side.
```
### Prompt — XSS / output encoding
```
Find every place user-supplied (or LLM-generated) content is rendered. Flag all uses of dangerouslySetInnerHTML, v-html, and innerHTML with unsanitized content and add DOMPurify where raw HTML is required. Confirm server-side template auto-escaping is on. Confirm a Content-Security-Policy exists as defense-in-depth. Show each risky render and its fix.
```
### Prompt — SSRF
```
Find any code that fetches a URL derived from user input (link previews, image proxies, import-from-URL, webhook testers, or an AI agent's URL fetching). Confirm it blocks private IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, ::1), allows only http/https, and resolves+checks the IP before requesting (guarding against DNS rebinding). If none exists, say so. Show fixes for any gap.
```
### Prompt — CSRF, CORS & security headers
```
Check three things. CSRF: are session cookies SameSite=Lax/Strict, or do all state-changing endpoints validate a CSRF token? CORS: is origin an explicit allowlist (never * or reflected), and is credentials:true never paired with a wildcard? Headers: are Content-Security-Policy, Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options, and Referrer-Policy set on every response via one global middleware (helmet / next.config.js)? List what's missing and the exact config.
```
### Prompt — Anti-scraping & bot protection
```
Review the app's resistance to automated scraping. Check: is there site-wide per-IP and per-user rate limiting (not just on auth); do list/search/data endpoints cap page size and require pagination so bulk extraction is slow; is sensitive data gated behind auth and metered per account; are there CAPTCHA/challenge points on high-value flows; is there bot/WAF protection at the edge; are datacenter IP ranges challenged on sensitive endpoints; is there monitoring/alerting for scraping patterns (sequential ID walking, high-volume single-client access)? List what's missing and recommend concrete additions.
```
### Prompt — Payments / webhooks (if applicable)
```
Audit Stripe (or other) payment handling. Confirm webhook signatures are verified on every request (invalid/missing -> 400); processed event IDs are stored for idempotency so duplicates are skipped; the full event lifecycle is handled (payment_intent.succeeded, invoice.payment_failed, customer.subscription.deleted, past_due); entitlements derive from verified server-side events, not a client redirect; and prices/amounts are validated server-side. List gaps and fixes.
```
### Prompt — File uploads (if applicable)
```
Audit file upload handling. Confirm: file type is validated by magic bytes not extension; files are renamed to UUIDs server-side; files are stored on a separate domain/bucket (S3/R2/GCS), never the app origin; size limits are enforced server-side; images are re-encoded to strip payloads/EXIF. Show fixes for any gap.
```
### Prompt — Error handling & deployment
```
Review error handling and production config for information disclosure. Confirm: a global error handler returns only generic messages to clients (no stack traces, SQL errors, file paths, or library names); full detail is logged server-side only; debug/dev mode is OFF in production; source maps are not deployed; custom 404/403/500 pages exist; errors return consistent JSON with correct status codes. Also confirm HTTPS with HSTS and that no secrets are hardcoded. Show every place internals could leak and the secure pattern for this framework.
```
### Prompt — Dependencies & supply chain
```
Audit all dependencies (package.json / requirements.txt / pyproject.toml / composer.json). For each: confirm it exists on the official registry with real download history and a maintained repo; check for typosquatting and slopsquatting (names close to popular packages, suspiciously new packages, < ~1000 weekly downloads, single unnamed maintainer); confirm exact versions are pinned and lock files committed; run npm audit / pip-audit and report critical/high issues; flag unused or unnecessary dependencies. List anything suspicious.
```
---
# 🧪 Manual Penetration Tests (do these yourself)
These verify the things AI can't confirm from inside the code — your *live* app's actual behavior. Do them before real users arrive. **If you only do five, do tests 15.**
### 1. Database isn't publicly queryable
Hit your REST endpoint with only your public/anon key (both are visible in your frontend):
```bash
curl "https://YOUR_PROJECT.supabase.co/rest/v1/users?select=*" -H "apikey: YOUR_ANON_KEY"
```
✅ PASS: empty array or permission error. ❌ FAIL: returns real user data.
(Firebase: try reading a collection while unauthenticated.)
### 2. Protected routes reject unauthenticated requests
In DevTools → Network, copy a request to a protected endpoint while logged in. Log out. Replay it without the session cookie/auth header.
✅ PASS: 401. ❌ FAIL: returns data.
Also: call an admin-only endpoint with a regular user session → should be **403**.
### 3. No secrets in git
```bash
git ls-files .env # PASS: no output
grep "\.env" .gitignore # PASS: shows a match
gitleaks detect --source . --verbose # PASS: no leaks (scans history too)
```
### 4. Can't access another user's data by changing an ID
Create User A and User B. As User A, request a resource that belongs to User B:
```
GET /api/resources/{user_b_id} -> should return 403
PUT /api/resources/{user_b_id} -> should return 403
```
Test *every* endpoint that takes a user-scoped ID (documents, orders, payments, profile). ❌ FAIL: you get B's data.
### 5. No secret keys in the browser
DevTools → Sources → search all files for `sk_`, `AKIA`, `Bearer`, `secret`, `private_key`. Then Network tab → watch for secret tokens sent from the client to third-party APIs.
✅ PASS: nothing. ❌ FAIL: any match.
### 6. Internal URLs are blocked (SSRF)
In any feature that fetches a user-supplied URL, submit:
```
http://127.0.0.1/ http://localhost/ http://[::1]/
http://10.0.0.1/ http://169.254.169.254/latest/meta-data/
```
✅ PASS: all rejected before any request. ❌ FAIL: any returns content. (Skip if no URL-fetching feature.)
### 7. Cross-origin form POSTs are blocked (CSRF)
While logged into your app, open an HTML file containing a hidden auto-submitting form that POSTs to one of your state-changing endpoints (e.g. change-email) from a different origin.
✅ PASS: action fails (403 / no effect). ❌ FAIL: it executes.
### 8. Security headers are present
```bash
curl -I https://yourapp.com | grep -i "content-security-policy\|strict-transport\|x-frame-options\|x-content-type"
```
✅ PASS: all present. Or check **securityheaders.com**.
### 9. CORS isn't wide open
```bash
curl -I -H "Origin: https://evil.com" https://yourapp.com/api/anything | grep -i "access-control-allow-origin"
```
✅ PASS: no header or your domain only. ❌ FAIL: `*` or echoes `https://evil.com`.
### 10. Login can't be brute-forced
```bash
for i in $(seq 1 50); do
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://yourapp.com/api/login \
-H "Content-Type: application/json" -d '{"email":"test@test.com","password":"wrong"}'
done
```
✅ PASS: starts returning 429 after a few. ❌ FAIL: all 50 return 401 unthrottled.
### 11. SQL injection doesn't work
In login, search, and filter inputs submit: `' OR '1'='1` and `'; DROP TABLE users; --`
✅ PASS: no unexpected data, no DB error leaked. ❌ FAIL: unexpected data or a SQL error appears.
### 12. XSS doesn't execute
In every text input and URL param submit: `<script>alert('XSS')</script>` and `<img src=x onerror=alert('XSS')>`
✅ PASS: shown as plain text. ❌ FAIL: an alert fires.
### 13. Stripe webhooks reject fakes
```bash
curl -X POST https://yourapp.com/api/webhook -H "Content-Type: application/json" \
-d '{"type":"payment_intent.succeeded","data":{"object":{"id":"pi_fake"}}}'
```
✅ PASS: 400 / signature error. ❌ FAIL: 200 and it processes. Also send the same valid event twice → if it processes both, idempotency is missing.
### 14. File uploads reject disguised files
Make `test.jpg` whose contents are `<script>alert('XSS')</script>`. Upload it normally, then visit its URL.
✅ PASS: rejected, or served as download, or on a different domain. ❌ FAIL: it executes on your origin.
### 15. Errors don't leak internals
Submit invalid JSON, non-existent IDs, and a lone `'`.
✅ PASS: generic "something went wrong." ❌ FAIL: stack traces, SQL, file paths, or library names appear.
### 16. Passwords are hashed properly
```bash
grep -rn "hashlib.md5\|hashlib.sha1\|createHash.*md5\|createHash.*sha1" --include="*.py" --include="*.js" --include="*.ts" ./
```
✅ PASS: no matches. bcrypt hashes start with `$2b$`; a 32-hex hash is MD5, 40-hex is SHA-1 (both bad).
### 17. Dependencies are real
For every package you don't recognize: confirm it exists on npmjs.com / pypi.org, check weekly downloads, publish date, and maintainer. Then:
```bash
npm audit # or: pip-audit
```
🚩 Red flags: < 1,000 weekly downloads, published in the last 30 days, lone unnamed maintainer, name close to a popular package.
### 18. (Bonus) Bulk scraping is throttled
As a logged-in user, script a loop pulling many list/detail pages or walking sequential IDs.
✅ PASS: you hit rate limits / pagination caps / challenges quickly. ❌ FAIL: you can pull thousands of records unthrottled.
### 19. (Bonus) AI agent abuse is capped
If you have an AI agent: send a rapid burst of requests, oversized inputs, and a few "ignore your instructions / reveal your system prompt / call an admin tool" attempts.
✅ PASS: per-user limits kick in, spend is capped, the agent refuses out-of-scope actions, no secrets/other-user data leak. ❌ FAIL: unthrottled spend or it follows injected instructions.
---
# 📜 Agent Rules (paste into `CLAUDE.md` / `AGENTS.md`)
Drop this block into your project root as `CLAUDE.md` (Claude Code) or `AGENTS.md` (Cursor / Copilot / Codex / Windsurf / Gemini). Not sure? Make both. Commit it. Your AI reads it on every task from then on and stops creating these issues in the first place.
```markdown
# Security Rules (non-negotiable, apply to all generated code)
## Secrets
- NEVER put API keys, DB credentials, or tokens in frontend code (src/, app/, pages/, components/, public/).
- NEVER put a secret in a client-exposed env var (NEXT_PUBLIC_*, VITE_*, REACT_APP_*) — those ship to the browser.
- NEVER hardcode credentials. Load secrets server-side from env vars only.
- .env MUST be in .gitignore before the first .env is created. Use .env.example with placeholders only.
## Database
- Enable Row Level Security on EVERY table before deploy. Default deny. Policies scoped to auth.uid().
- NEVER use an RLS policy of USING(true) or FOR ALL without a WHERE condition.
- Firebase rules MUST require request.auth != null and scope to request.auth.uid.
- Service-role / admin DB keys NEVER appear in client code.
- NEVER deserialize untrusted data (no pickle.loads on user input). Use JSON for network data.
## Auth & authorization
- EVERY protected route gets auth middleware that runs BEFORE the handler. Unauthenticated -> 401.
- EVERY route taking a resource ID MUST verify ownership (current_user.id == resource.owner_id) as a SEPARATE check, on reads AND writes. Fail -> 403.
- Admin routes verify admin role -> 403 for non-admins.
- Prevent mass assignment: whitelist accepted fields; never spread the request body into a DB model. Users can't set role/is_admin/balance/user_id.
- Session cookies: httpOnly:true, secure:true, sameSite:'lax'. Tokens expire and are invalidated server-side on logout.
- Passwords: bcrypt/argon2/scrypt only. NEVER MD5/SHA-1/SHA-256.
- Don't leak account existence on login/signup/reset (identical responses).
## Input / output
- NEVER concatenate user input into SQL. ALWAYS parameterized queries / ORM methods.
- NEVER use dangerouslySetInnerHTML/v-html/innerHTML with unsanitized content. Sanitize with DOMPurify.
- ALL validation server-side (client-side is UX only). Validate type/length/format/range.
- No command injection (no user input into exec/subprocess/eval), no path traversal, no open redirects, no SSTI/XXE.
- Enforce request body size limits.
## URL fetching (SSRF)
- If fetching user-supplied URLs: block 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, ::1; allow only http/https; resolve and check the IP BEFORE the request.
## API & abuse
- Per-user/per-key rate limits + quotas on ALL endpoints, not just auth.
- List/search endpoints: max page size + required pagination. No endpoint returns all rows.
- Spend caps + circuit breakers on every metered downstream service (LLM, SMS, email, cloud).
- Don't trust X-Forwarded-For for rate limiting unless behind a trusted proxy.
## AI / LLM agent (if present)
- Treat model output as untrusted: never eval/SQL/shell it without validation.
- Keep authorization and business logic in code, not the prompt. Assume users can extract the system prompt.
- Treat retrieved/external content as untrusted (indirect prompt injection). Scope tools to the current user with least privilege. Dangerous actions need confirmation. Sandbox any code execution (no secrets/network).
- Enforce per-user token/request quotas, max input/output length, and an LLM spend cap. Moderate input and output. Sanitize agent output rendered in the browser.
## Headers / CORS / CSRF
- Set CSP, Strict-Transport-Security, X-Frame-Options:DENY, X-Content-Type-Options:nosniff, Referrer-Policy on ALL responses via one global middleware (helmet / next.config.js).
- CORS origin = explicit allowlist. NEVER '*', never reflected, never wildcard + credentials:true.
- Session cookies SameSite=Lax/Strict OR CSRF tokens on all state-changing endpoints.
## Files / payments / errors / deps
- Uploads: validate by magic bytes, rename to UUID, store on a separate bucket/domain, enforce size server-side.
- Stripe webhooks: verify signature every request (-> 400 if bad), idempotent via stored event IDs, handle failure events. Entitlements from verified server-side events, not client redirects.
- Errors: global handler, generic client messages only, full detail to server logs, debug OFF in production, no source maps in prod.
- Dependencies: verify each package exists on the official registry (guard against typo/slopsquatting) BEFORE installing. Pin exact versions. Commit lock files.
```
---
# 🎯 Where to start (priority order)
You don't have to do everything at once. In order:
1. **TIER 0 (the five).** Today. These are what get apps breached.
2. **Sections 14** — secrets, database, auth, access control. The foundation.
3. **Sections 57** — API abuse, anti-scraping, AI agent. Your three stated concerns.
4. **Sections 818** — injection, XSS, SSRF, CSRF, CORS, headers, rate limiting, uploads, payments, errors, dependencies.
5. **Sections 1921** — infrastructure, monitoring, data protection. The operational layer that catches what code review misses.
6. **Run the manual tests** against your live app. Then re-run after every significant change.
---
# 🧠 Final word
This checklist closes the gap between "the AI made it work" and "the AI made it safe" — and it covers the two things off-the-shelf checklists skip: **scraping** and **API/cost abuse**, plus the **AI-agent attack surface** if your product is an agent.
But repeat after me: **there is no unhackable.** What you've built here is a high wall with cameras — enough to send casual attackers and bored kids looking for an easier target, and enough to detect and slow down the serious ones. When you have real users and real revenue, **hire a penetration tester.** A human actively trying to break your app will find things no checklist can, and that's worth far more than the cost.
Ship it locked down. Then keep watching the logs.
---
*Built by merging the vibe-check ruleset (MIT) and Sherlock Forensics' security prompts, expanded with anti-scraping, API-abuse, AI/LLM-agent, infrastructure, and monitoring coverage. Sources documented real breaches across 5,600+ scanned vibe-coded apps. This is a strong first pass — not a substitute for a professional audit.*

290
SECURITY.md Normal file
View File

@ -0,0 +1,290 @@
# SECURITY.md — Security Rules for This Project
> **To the AI agent reading this:** These are **mandatory, non-negotiable rules**. Follow them on every change you make, even when the user doesn't mention security. When a requested feature conflicts with a rule here, implement the secure version and tell the user what you did and why. If you are unsure whether something is safe, **stop and ask** rather than guessing. Never disable a security control to "make it work" — fix the underlying cause instead.
This file exists because apps built fast (including AI-assisted ones) repeatedly ship the same handful of vulnerabilities. The goal is an app that is "immune" to the common classes of attack by **default**, not as an afterthought.
---
## 0. The Golden Rules (read these first)
1. **The frontend is public.** Anything in client-side code, network responses, or the browser is visible to and editable by an attacker. Never trust it. All real security must live on the server / database.
2. **Never trust input.** Validate, sanitize, and authorize *every* input on the server — query params, headers, body, file uploads, cookies, and webhook payloads.
3. **Authenticate, then authorize, on every request.** Logging in is not enough. Check *this specific user is allowed to do this specific action on this specific resource* — server-side, every time.
4. **Secrets stay on the server.** No keys, tokens, passwords, or connection strings in frontend code, repos, or logs. Ever.
5. **Fail closed.** When something is ambiguous or errors out, deny access. Default to the most restrictive option.
6. **Least privilege everywhere.** Every key, role, token, and DB account gets the minimum permissions it needs and nothing more.
---
## 1. Secrets & Credentials
**NEVER**
- Hardcode API keys, passwords, JWT secrets, DB URLs, or private tokens anywhere in source.
- Put secrets in frontend/client code or in any `NEXT_PUBLIC_`, `VITE_`, `REACT_APP_`, or otherwise client-exposed env var.
- Commit `.env`, `.env.local`, credential files, `*.pem`, or service-account JSON to git.
- Log secrets, tokens, or full request bodies that may contain them.
- Reuse the same secret across dev/staging/prod.
**ALWAYS**
- Load secrets from server-side environment variables only.
- Add `.env*`, `*.pem`, `*.key`, `secrets/`, and credential files to `.gitignore` *before* the first commit.
- Provide a `.env.example` with key **names only** and dummy values.
- Use a distinct, high-entropy (32+ random bytes) value for each signing/encryption secret.
- Assume any secret that ever touched the frontend or a public repo is **compromised** — rotate it.
- Use a secrets manager (or the platform's encrypted env store) in production, not files on disk.
---
## 2. Authentication
**NEVER**
- Roll your own crypto, password hashing, or session logic when a vetted library/provider exists.
- Store passwords in plaintext or with fast/weak hashes (MD5, SHA1, unsalted SHA256).
- Trust a `userId`, `role`, `isAdmin`, or `email` value that came from the client to decide who someone is.
- Put auth state only in `localStorage` and treat its presence as proof of identity.
- Leave default/seed credentials (`admin/admin`) in any environment.
- Return "user not found" vs "wrong password" differently (enables user enumeration).
**ALWAYS**
- Use a maintained auth library/provider (e.g. the platform's auth, Auth.js, Lucia, Clerk, Supabase Auth) and follow its server-side session verification.
- Hash passwords with bcrypt, scrypt, or Argon2 with a proper cost factor.
- Verify the session/token **on the server** for every protected request and derive identity from it — never from request body params.
- Enforce a password policy + check against known-breached passwords where possible.
- Implement account lockout / throttling on repeated failed logins.
- Offer/encourage MFA for sensitive accounts.
- Expire sessions, rotate on privilege change, and invalidate on logout server-side.
---
## 3. Authorization (the #1 vibe-coded flaw)
> Most rapidly-built apps authenticate but barely authorize. This is where IDOR, broken access control, and data leaks come from.
**NEVER**
- Hide a button/route on the frontend and call it "protected." UI hiding is not access control.
- Use sequential/guessable IDs as the *only* thing protecting a resource (`/api/orders/1043` → an attacker just tries `1044`). This is **IDOR**.
- Assume that because the UI only sends valid requests, the server only receives valid requests.
- Let one user's token read/modify another user's data because the query didn't filter by owner.
**ALWAYS**
- On every data access, check ownership/permission server-side: `WHERE owner_id = :current_user` (or equivalent), not just `WHERE id = :id`.
- Re-check role/permission for every privileged action on the server, on every call.
- Enforce authorization at the **lowest layer possible** (DB row-level security + API layer), so a missed check in one place doesn't expose everything.
- Use unguessable IDs (UUIDs/ULIDs) for resources, *and still* authorize them.
- Centralize authorization logic so it's consistent and auditable, not copy-pasted per route.
---
## 4. Database Security (Supabase / Firebase / direct DB)
> Vibe-coded apps frequently expose the DB straight to the browser with permissive rules. This is catastrophic.
**NEVER**
- Ship Supabase/Postgres tables with **Row Level Security (RLS) disabled** on anything holding user data.
- Use Firebase/Firestore rules like `allow read, write: if true;` or test-mode rules in production.
- Expose a service-role / admin DB key to the client (the anon/public key is the only client-side key, and even that needs RLS behind it).
- Build SQL by string concatenation or template interpolation with user input.
- Run app queries as a superuser/admin DB account.
**ALWAYS**
- Enable RLS on every table and write explicit policies scoped to the authenticated user.
- Write Firestore/RTDB rules that check `request.auth` and validate ownership and shape.
- Use parameterized queries / prepared statements / a query builder / ORM bindings — **always** (prevents SQL injection).
- Give the app a least-privilege DB role (no DROP/ALTER, only needed tables).
- Validate data shape and types at the DB boundary, not just the UI.
- Keep the service-role key strictly server-side for trusted admin operations.
---
## 5. Input Validation & Injection
**NEVER**
- Pass user input directly into SQL, shell commands, `eval`, template engines, `dangerouslySetInnerHTML`, file paths, or `Function()`.
- Rely on frontend validation as a security boundary (it's UX only).
- Build file paths from user input without normalizing (enables **path traversal**, `../../etc/passwd`).
**ALWAYS**
- Validate every input server-side against a strict schema (e.g. Zod, Pydantic, Joi) — type, length, format, allowed values.
- Allow-list rather than block-list whenever possible.
- Use parameterized queries for SQL and safe APIs for OS/command operations (avoid shelling out with user input at all).
- Reject unexpected fields to prevent **mass assignment** (don't blindly spread `req.body` into a DB record — pick allowed fields explicitly).
- Canonicalize and confine file paths to an intended directory.
---
## 6. Output / XSS / Content
**NEVER**
- Render untrusted content as raw HTML (`dangerouslySetInnerHTML`, `innerHTML`, `v-html`, `{{{ }}}`) without sanitizing.
- Reflect user input into responses unescaped.
- Inject user-controlled values into `<script>`, inline event handlers, or URLs (`javascript:`).
**ALWAYS**
- Rely on framework auto-escaping (React/Vue/Svelte escape by default — keep it that way).
- Sanitize any HTML you *must* render with a maintained sanitizer (e.g. DOMPurify) and a strict allow-list.
- Set a strong **Content-Security-Policy** to limit script sources.
- Encode output for the correct context (HTML, attribute, JS, URL).
---
## 7. APIs, CORS & Rate Limiting
**NEVER**
- Set `Access-Control-Allow-Origin: *` together with credentials, or wildcard CORS on authenticated APIs.
- Ship public endpoints with no rate limiting (enables brute force, scraping, credential stuffing, cost-blowup, and DoS).
- Leave debug/admin/internal endpoints reachable in production.
- Return different responses that leak whether a record exists when it shouldn't.
**ALWAYS**
- Restrict CORS to an explicit allow-list of known origins.
- Rate-limit by IP and by user/account on auth, write, and expensive endpoints; add exponential backoff/lockout for auth.
- Require auth on every non-public endpoint and verify it server-side.
- Validate `Content-Type` and reject unexpected methods.
- Protect state-changing requests against **CSRF** (SameSite cookies + CSRF tokens, or token-based auth not stored in cookies).
- Treat webhooks as untrusted: verify signatures before acting.
---
## 8. Sessions, Tokens & JWTs
**NEVER**
- Accept JWTs with `alg: none` or let the client choose the algorithm.
- Sign JWTs with a weak/guessable/shared secret.
- Issue tokens with no expiry, or store sensitive long-lived tokens in `localStorage` (XSS-readable).
- Trust JWT claims without verifying the signature server-side.
**ALWAYS**
- Pin the expected algorithm and verify the signature on every request.
- Use short-lived access tokens + rotating refresh tokens.
- Prefer `httpOnly`, `Secure`, `SameSite` cookies for session tokens.
- Include and validate `exp`, `iss`, `aud`; revoke on logout/compromise.
---
## 9. File Uploads
**NEVER**
- Trust the client-provided filename, extension, or MIME type.
- Store uploads in a web-served directory where they can be executed.
- Allow arbitrary file types or unbounded file sizes.
**ALWAYS**
- Validate type by content (magic bytes), enforce a size limit, and allow-list extensions.
- Generate a new random server-side filename; never use the user's path.
- Store outside the web root or in object storage with no-execute and least-privilege access.
- Scan uploads where feasible and serve them from a separate domain/sandbox with `Content-Disposition`.
---
## 10. HTTP Security Headers & Transport
**ALWAYS** set (or have the framework/host set) at minimum:
- `Strict-Transport-Security` (HSTS) — and enforce HTTPS everywhere; redirect HTTP→HTTPS.
- `Content-Security-Policy` — restrict scripts/styles/connect sources.
- `X-Content-Type-Options: nosniff`
- `X-Frame-Options: DENY` (or CSP `frame-ancestors`) to prevent clickjacking.
- `Referrer-Policy: strict-origin-when-cross-origin`
- Sensible `Permissions-Policy`.
**NEVER** serve auth or sensitive data over plain HTTP, or mix HTTP content into an HTTPS app.
---
## 11. Error Handling & Logging
**NEVER**
- Return stack traces, SQL errors, file paths, or internal details to the client.
- Log passwords, tokens, full card numbers, or other secrets/PII.
- Run production with debug mode on.
**ALWAYS**
- Return generic error messages to users; log detail server-side with correlation IDs.
- Log security-relevant events (auth failures, access denials, privilege changes) for audit.
- Scrub/redact sensitive fields before logging.
---
## 12. Dependencies & Supply Chain
**NEVER**
- Add a dependency you don't need, or pull random unvetted packages for a one-liner.
- Ignore known-vulnerable dependency warnings.
**ALWAYS**
- Run `npm audit` / `pip-audit` / `osv-scanner` (or equivalent) and fix high/critical issues.
- Pin versions and use a lockfile; keep dependencies updated.
- Verify package names carefully (watch for typosquats).
- Enable automated dependency security updates (e.g. Dependabot) where possible.
---
## 13. Sensitive Data & Privacy
**NEVER**
- Collect or store more personal data than the feature needs.
- Store sensitive data (PII, payment, health) unencrypted at rest.
- Send sensitive data to third-party services/analytics without need and consent.
**ALWAYS**
- Encrypt sensitive data at rest and in transit.
- Apply least-privilege access to PII; mask/redact in UIs and logs.
- Offboard payment handling to a PCI-compliant provider (e.g. Stripe) rather than touching card data yourself.
- Provide data deletion/export paths where regulations require.
---
## 14. AI / LLM-Specific (if the app calls an LLM)
**NEVER**
- Concatenate untrusted user input directly into a system prompt and treat the model output as trusted (**prompt injection**).
- Give the model unrestricted tools/DB/shell access driven by user-controlled text.
- Expose your LLM provider API key to the client.
**ALWAYS**
- Treat model output as untrusted input — validate and authorize any action it triggers.
- Keep the LLM key server-side and put your own rate limits + spend caps around it.
- Separate trusted instructions from untrusted user content; constrain tool use with allow-lists and per-action authorization.
- Filter/limit what data the model can retrieve based on the requesting user's permissions.
---
## 15. Server-Side Request & Redirect Safety
**NEVER**
- Fetch a user-supplied URL from the server without restriction (**SSRF** — can hit internal metadata endpoints / private network).
- Redirect to a user-supplied URL without validation (**open redirect** — aids phishing).
**ALWAYS**
- Allow-list domains/schemes for any server-side fetch; block private/internal IP ranges and metadata IPs.
- Validate redirect targets against an allow-list or use relative paths only.
---
## Pre-Ship Security Checklist
Run through this before any deploy. Treat unchecked items as blockers.
- [ ] No secrets in code, repo history, logs, or client bundles; `.env*` gitignored; keys rotated if ever exposed.
- [ ] Auth verified server-side on every protected route; passwords hashed with bcrypt/argon2; no default creds.
- [ ] Every data read/write is authorized by ownership/role server-side (no IDOR); privileged actions re-checked.
- [ ] RLS / DB rules enabled and scoped per-user; service-role key server-side only; no `if true` rules.
- [ ] All queries parameterized; all input validated server-side against a schema; mass assignment prevented.
- [ ] Output escaped/sanitized; CSP set; no unsanitized raw-HTML rendering.
- [ ] CORS locked to known origins; rate limiting on auth/write/expensive endpoints; CSRF handled; webhooks signature-verified.
- [ ] JWTs verify signature + pinned alg + expiry; session cookies `httpOnly`/`Secure`/`SameSite`.
- [ ] File uploads validated by content, size-limited, randomly named, stored no-execute.
- [ ] Security headers + HTTPS enforced (HSTS, nosniff, frame-ancestors, referrer-policy).
- [ ] Generic error messages to users; debug off in prod; no secrets/PII in logs.
- [ ] Dependency audit clean (no high/critical); lockfile committed.
- [ ] Sensitive data minimized + encrypted; payments via compliant provider.
- [ ] LLM key server-side; model output treated as untrusted; SSRF/open-redirect protections in place.
---
## How to use this file with the AI agent
- Keep this file at the project root as `SECURITY.md` and reference it in your `CLAUDE.md` / agent instructions: *"Follow all rules in SECURITY.md on every change."*
- When asking for a feature, you can add: *"Implement this per SECURITY.md and flag any rule it touches."*
- Periodically ask: *"Audit the current codebase against SECURITY.md and list violations by severity."*
- If the agent ever proposes weakening a control for convenience, that's a red flag — ask for a secure alternative instead.

24
admin/Dockerfile Normal file
View File

@ -0,0 +1,24 @@
# Admin panel image. Build context = project ROOT (needs backend/db.py + admin/).
# docker build -f admin/Dockerfile -t didvan-admin .
# Keep this OFF public networks (it exposes user PII + the data-source token).
# Base image from Liara's mirror — Docker Hub is blocked from Iran builds.
FROM docker-mirror.liara.ir/library/python:3.12-slim
ENV PYTHONUNBUFFERED=1 \
PYTHONUTF8=1 \
PIP_NO_CACHE_DIR=1 \
DB_PATH=/data/asianmetal.db
WORKDIR /app
# admin only needs a few libs (no Playwright/Chromium)
RUN pip install --no-cache-dir fastapi "uvicorn[standard]" pydantic requests python-multipart
# admin_app imports nothing from backend, but copy both so paths line up
COPY admin ./admin
COPY backend ./backend
RUN mkdir -p /data
EXPOSE 8001
CMD ["python", "-m", "uvicorn", "admin_app:app", "--host", "0.0.0.0", "--port", "8001", "--app-dir", "admin"]

696
admin/admin_app.py Normal file
View File

@ -0,0 +1,696 @@
"""
Didvan standalone admin panel (independent FastAPI app, default port 8001).
Run: uvicorn admin_app:app --app-dir admin --port 8001
Auth: HTTP Basic. Set ADMIN_USER / ADMIN_PASSWORD env vars (defaults below).
Three sections:
- کاربران : registered users (name / email / phone) from the `users` table
- توکن منبع داده : view & update the scraping token (stored in app_config; sync.py reads it)
- سلامت سرویسها : last-scrape recency per service + "test scrape" that validates the
SteelStatista token by fetching a sample symbol's latest price
"""
import os
import re
import secrets
import random
from datetime import datetime, timezone
import sqlite3
import requests
from fastapi import FastAPI, Depends, HTTPException, UploadFile, File, Form, Query
from fastapi.responses import HTMLResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from pydantic import BaseModel
DB_PATH = os.environ.get("DB_PATH", r"C:\Users\DATIS STAR\asianmetal.db")
# Where report PDFs live — must match the backend's REPORTS_DIR so uploads show
# up in the dashboard's reports tab (on the VPS both point at the shared /data disk).
REPORTS_DIR = os.environ.get("REPORTS_DIR", r"C:\Users\DATIS STAR\Desktop\asianmetal_reports")
MAX_PDF_BYTES = 30 * 1024 * 1024
ADMIN_USER = os.environ.get("ADMIN_USER", "admin")
# No insecure default — the admin panel exposes user PII + the data-source token,
# so a password MUST be provided via the environment (fail closed).
ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD")
if not ADMIN_PASSWORD:
raise RuntimeError(
"ADMIN_PASSWORD environment variable is required (no default). "
"Set it before starting the admin app, e.g. $env:ADMIN_PASSWORD='a-strong-secret'."
)
DATA_API_BASE = "https://api.SteelStatista.com"
CATEGORY_IDS = [
9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53,
54, 55, 56, 57, 65, 67, 68, 70, 71, 72, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85,
87, 88, 89, 90, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105,
]
app = FastAPI(title="Didvan Admin")
security = HTTPBasic()
def require_admin(creds: HTTPBasicCredentials = Depends(security)) -> str:
ok = secrets.compare_digest(creds.username, ADMIN_USER) and secrets.compare_digest(
creds.password, ADMIN_PASSWORD
)
if not ok:
raise HTTPException(status_code=401, detail="Unauthorized", headers={"WWW-Authenticate": "Basic"})
return creds.username
def conn():
c = sqlite3.connect(DB_PATH, timeout=10)
c.row_factory = sqlite3.Row
c.execute("PRAGMA journal_mode=WAL")
return c
def _get_token():
try:
with conn() as c:
row = c.execute("SELECT value FROM app_config WHERE key='asianmetal_token'").fetchone()
if row and row["value"]:
return row["value"]
except Exception:
pass
return None
def _age_minutes(ts):
if not ts:
return None
try:
dt = datetime.strptime(ts, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
return round((datetime.now(timezone.utc) - dt).total_seconds() / 60, 1)
except Exception:
return None
# ───────────────────────────── APIs ─────────────────────────────
@app.get("/api/users")
def api_users(_: str = Depends(require_admin)):
with conn() as c:
rows = c.execute(
"SELECT id, name, email, phone, organization, "
"COALESCE(status,'pending') AS status, created_at "
"FROM users ORDER BY (COALESCE(status,'pending')='pending') DESC, id DESC"
).fetchall()
return [dict(r) for r in rows]
def _set_user_status(uid: int, status: str, _: str = Depends(require_admin)):
with conn() as c:
cur = c.execute("UPDATE users SET status=? WHERE id=?", (status, uid))
if cur.rowcount == 0:
raise HTTPException(status_code=404, detail="کاربر یافت نشد")
return {"ok": True, "id": uid, "status": status}
@app.post("/api/users/{uid}/approve")
def api_approve(uid: int, admin: str = Depends(require_admin)):
return _set_user_status(uid, "approved", admin)
@app.post("/api/users/{uid}/reject")
def api_reject(uid: int, admin: str = Depends(require_admin)):
# rejecting also kills any active session so access is revoked immediately
with conn() as c:
c.execute("DELETE FROM sessions WHERE user_id=?", (uid,))
return _set_user_status(uid, "rejected", admin)
class TokenIn(BaseModel):
token: str
@app.get("/api/token")
def api_token(_: str = Depends(require_admin)):
t = _get_token()
masked = (t[:14] + "" + t[-10:]) if t and len(t) > 30 else (t or "")
updated = None
try:
with conn() as c:
row = c.execute("SELECT value FROM app_config WHERE key='asianmetal_token_updated'").fetchone()
updated = row["value"] if row else None
except Exception:
pass
return {"set": bool(t), "masked": masked, "updated_at": updated}
@app.post("/api/token")
def api_set_token(data: TokenIn, _: str = Depends(require_admin)):
tok = (data.token or "").strip()
if not tok:
return {"ok": False, "error": "توکن خالی است"}
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
with conn() as c:
c.execute(
"INSERT INTO app_config(key,value) VALUES('asianmetal_token',?) "
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
(tok,),
)
c.execute(
"INSERT INTO app_config(key,value) VALUES('asianmetal_token_updated',?) "
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
(now,),
)
return {"ok": True, "updated_at": now}
def _count_change(c, table):
try:
row = c.execute(f"SELECT MAX(fetched_at) m, COUNT(*) n FROM {table}").fetchone()
return row["n"], row["m"]
except Exception:
return 0, None
@app.get("/api/health")
def api_health(_: str = Depends(require_admin)):
"""Service health. TGJU services use a heartbeat (last completed scrape cycle),
NOT MAX(fetched_at) because dedup means the table only changes when a price
moves, so a quiet market would otherwise look like a dead scraper."""
out = []
with conn() as c:
hb = None
try:
row = c.execute("SELECT value FROM app_config WHERE key='tgju_last_scrape'").fetchone()
hb = row["value"] if row else None
except Exception:
pass
hb_age = _age_minutes(hb)
# TGJU bundle (currency/gold/coin) — freshness from the shared heartbeat
for table, label in [
("currency_prices", "بازار داخلی — ارز"),
("gold_prices", "بازار داخلی — طلا"),
("coin_prices", "بازار داخلی — سکه"),
]:
n, change = _count_change(c, table)
out.append({
"key": table, "label": label, "last": hb, "age_min": hb_age,
"count": n, "ok": hb_age is not None and hb_age <= 3, "changed": change,
})
# Commodity (Trading Economics) — its own scraper, table timestamp is fine
n, change = _count_change(c, "te_prices")
c_age = _age_minutes(change)
out.append({
"key": "commodity", "label": "کامودیتی", "last": change, "age_min": c_age,
"count": n, "ok": c_age is not None and c_age <= 90, "changed": change,
})
# Metals & steel — daily batch via sync.py (SteelStatista)
try:
row = c.execute("SELECT MAX(date) m, COUNT(*) n FROM prices").fetchone()
out.append({
"key": "metals", "label": "فلزات و فولاد (روزانه)", "last": row["m"],
"age_min": None, "count": row["n"], "ok": True, "daily": True,
})
except Exception:
pass
return out
def _yz_headers(tok):
return {
"Authorization": f"Bearer {tok}",
"accept": "application/json",
"lang": "fa",
"platform": "web",
"referer": "https://SteelStatista.com/",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
}
@app.post("/api/test-scrape")
def api_test_scrape(_: str = Depends(require_admin)):
"""Validate the data-source token by fetching latest prices for a random
sample of ~12 symbols across random categories."""
tok = _get_token()
if not tok:
return {"ok": False, "error": "توکنی تنظیم نشده — اول توکن را در تب «توکن» ذخیره کنید."}
headers = _yz_headers(tok)
try:
pool = []
for cid in random.sample(CATEGORY_IDS, min(6, len(CATEGORY_IDS))):
try:
r = requests.get(f"{DATA_API_BASE}/Symbol?categoryId={cid}", headers=headers, timeout=12)
if r.status_code == 401:
return {"ok": False, "error": "توکن منقضی یا نامعتبر است (401) — باید عوضش کنی."}
r.raise_for_status()
pool.extend(r.json() or [])
except Exception:
continue
if not pool:
return {"ok": False, "error": "هیچ نمادی برنگشت."}
items = []
for s in random.sample(pool, min(12, len(pool))):
try:
r2 = requests.get(f"{DATA_API_BASE}/Symbol/{s['id']}", headers=headers, timeout=12)
r2.raise_for_status()
prices = r2.json().get("symbolPrices", [])
if not prices:
continue
last = max(prices, key=lambda p: p["date"])
items.append({
"title": s.get("title"),
"date": last["date"][:10],
"mid": last.get("mid"),
"low": last.get("low"),
"high": last.get("high"),
})
except Exception:
continue
if not items:
return {"ok": False, "error": "قیمتی دریافت نشد."}
items.sort(key=lambda x: x["title"] or "")
return {"ok": True, "count": len(items), "items": items}
except Exception as e:
return {"ok": False, "error": f"خطا در اتصال به منبع داده: {e}"}
_DATASETS = {
"currency": ("currency_prices", "بازار داخلی — ارز", "fetched_at"),
"gold": ("gold_prices", "بازار داخلی — طلا/نقره", "fetched_at"),
"coin": ("coin_prices", "بازار داخلی — سکه", "fetched_at"),
"commodity": ("te_prices", "کامودیتی", "fetched_at"),
"metals": ("prices", "فلزات و فولاد", "date"),
}
@app.get("/api/reports")
def api_reports(_: str = Depends(require_admin)):
out = []
with conn() as c:
for key, (table, label, datecol) in _DATASETS.items():
try:
row = c.execute(
f"SELECT COUNT(*) n, MIN({datecol}) mn, MAX({datecol}) mx FROM {table}"
).fetchone()
out.append({"key": key, "label": label, "table": table,
"count": row["n"], "first": row["mn"], "last": row["mx"]})
except Exception:
out.append({"key": key, "label": label, "table": table,
"count": 0, "first": None, "last": None})
return out
@app.get("/api/export")
def api_export(dataset: str, _: str = Depends(require_admin)):
import csv
import io
from fastapi.responses import Response
if dataset not in _DATASETS:
raise HTTPException(status_code=404, detail="dataset not found")
table, _label, datecol = _DATASETS[dataset]
with conn() as c:
if dataset == "metals":
rows = c.execute(
"SELECT grp, category, title, date, low, mid, high FROM prices "
"WHERE date = (SELECT MAX(date) FROM prices) ORDER BY grp, category, title"
).fetchall()
else:
rows = c.execute(
f"SELECT * FROM {table} WHERE {datecol} = (SELECT MAX({datecol}) FROM {table})"
).fetchall()
buf = io.StringIO()
if rows:
w = csv.writer(buf)
w.writerow(rows[0].keys())
for r in rows:
w.writerow([r[k] for k in r.keys()])
return Response(
content="" + buf.getvalue(),
media_type="text/csv; charset=utf-8",
headers={"Content-Disposition": f'attachment; filename="{dataset}_latest.csv"'},
)
# ─────────────────────── Report PDF upload ───────────────────────
def _ensure_api_usage_table(c):
c.execute("""
CREATE TABLE IF NOT EXISTS api_usage_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
partner TEXT,
token_hash TEXT,
method TEXT NOT NULL,
path TEXT NOT NULL,
query TEXT,
status_code INTEGER,
client_ip TEXT,
user_agent TEXT,
created_at TEXT NOT NULL
)
""")
c.execute("CREATE INDEX IF NOT EXISTS idx_api_usage_created ON api_usage_logs(created_at)")
c.execute("CREATE INDEX IF NOT EXISTS idx_api_usage_partner ON api_usage_logs(partner)")
c.execute("CREATE INDEX IF NOT EXISTS idx_api_usage_path ON api_usage_logs(path)")
@app.get("/api/api-usage")
def api_usage(limit: int = Query(100, ge=1, le=500), _: str = Depends(require_admin)):
with conn() as c:
_ensure_api_usage_table(c)
summary = c.execute("""
SELECT
COALESCE(partner, 'unknown') AS partner,
COUNT(*) AS requests,
SUM(CASE WHEN status_code BETWEEN 200 AND 399 THEN 1 ELSE 0 END) AS ok,
SUM(CASE WHEN status_code >= 400 THEN 1 ELSE 0 END) AS errors,
MAX(created_at) AS last_seen
FROM api_usage_logs
GROUP BY COALESCE(partner, 'unknown')
ORDER BY requests DESC, partner
""").fetchall()
rows = c.execute("""
SELECT id, partner, substr(token_hash, 1, 12) AS token_hash_prefix,
method, path, query, status_code, client_ip, user_agent, created_at
FROM api_usage_logs
ORDER BY id DESC
LIMIT ?
""", (limit,)).fetchall()
return {"summary": [dict(r) for r in summary], "rows": [dict(r) for r in rows]}
def _safe_folder(folder: str):
"""One path segment, no traversal, filesystem-safe."""
f = (folder or "").strip().replace("\\", "/").split("/")[0].strip(". ")
f = re.sub(r'[<>:"|?*\x00-\x1f]', "", f)
return f or None
@app.get("/api/report-folders")
def report_folders(_: str = Depends(require_admin)):
base = os.path.realpath(REPORTS_DIR)
if not os.path.isdir(base):
return {"dir": REPORTS_DIR, "folders": []}
out = []
for e in os.scandir(base):
if e.is_dir():
n = sum(1 for f in os.scandir(e.path) if f.is_file() and f.name.lower().endswith(".pdf"))
out.append({"name": e.name, "count": n})
out.sort(key=lambda x: x["name"])
return {"dir": REPORTS_DIR, "folders": out}
@app.post("/api/upload-report")
async def upload_report(folder: str = Form(...), file: UploadFile = File(...), _: str = Depends(require_admin)):
safe = _safe_folder(folder)
if not safe:
raise HTTPException(status_code=400, detail="نام پوشه نامعتبر است")
name = os.path.basename(file.filename or "")
if not name.lower().endswith(".pdf"):
raise HTTPException(status_code=400, detail="فقط فایل PDF مجاز است")
name = re.sub(r"[^0-9A-Za-z ._()\-؀-ۿ]", "_", name)
data = await file.read()
if len(data) > MAX_PDF_BYTES:
raise HTTPException(status_code=400, detail="حجم فایل بیش از ۳۰ مگابایت است")
if data[:5] != b"%PDF-": # validate by content, not just extension
raise HTTPException(status_code=400, detail="محتوای فایل PDF معتبر نیست")
base = os.path.realpath(REPORTS_DIR)
dest_dir = os.path.realpath(os.path.join(base, safe))
if dest_dir != base and not dest_dir.startswith(base + os.sep):
raise HTTPException(status_code=400, detail="مسیر نامعتبر")
os.makedirs(dest_dir, exist_ok=True)
with open(os.path.join(dest_dir, name), "wb") as fh:
fh.write(data)
return {"ok": True, "folder": safe, "name": name, "size": len(data)}
# ───────────────────────────── UI ─────────────────────────────
@app.get("/", response_class=HTMLResponse)
def home(_: str = Depends(require_admin)):
return _HTML
_HTML = """<!doctype html>
<html lang="fa" dir="rtl">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>پنل ادمین دیدوان</title>
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;600;700&display=swap" rel="stylesheet"/>
<style>
:root{--bg:#f5f6fb;--surface:#fff;--ink:#1d2330;--muted:#5b6172;--border:#e4e6ef;--primary:#5b4bdb;--ok:#16a34a;--bad:#dc2626;--warn:#d97706;}
*{box-sizing:border-box}
body{margin:0;font-family:Vazirmatn,system-ui,sans-serif;background:var(--bg);color:var(--ink);}
header{background:var(--surface);border-bottom:1px solid var(--border);padding:16px 24px;display:flex;align-items:center;gap:10px;position:sticky;top:0;z-index:5}
.logo{width:34px;height:34px;border-radius:9px;background:rgba(91,75,219,.12);border:1px solid rgba(91,75,219,.35);display:grid;place-items:center;color:var(--primary);font-weight:700}
h1{font-size:16px;margin:0}
.sub{font-size:11px;color:var(--muted)}
.wrap{max-width:1000px;margin:0 auto;padding:24px}
.tabs{display:flex;gap:6px;background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:6px;width:fit-content;margin-bottom:20px}
.tab{padding:8px 18px;border:0;background:none;border-radius:8px;font:inherit;font-weight:600;color:var(--muted);cursor:pointer}
.tab.active{background:var(--primary);color:#fff}
.card{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:20px;margin-bottom:16px;box-shadow:0 1px 2px rgba(16,24,40,.04)}
h2{font-size:15px;margin:0 0 14px}
table{width:100%;border-collapse:collapse;font-size:13px}
th,td{text-align:right;padding:10px 12px;border-bottom:1px solid var(--border)}
th{font-size:11px;color:var(--muted);font-weight:600}
td.mono,th.mono{font-family:ui-monospace,monospace;direction:ltr;text-align:left}
.btn{background:var(--primary);color:#fff;border:0;border-radius:9px;padding:10px 18px;font:inherit;font-weight:600;cursor:pointer}
.btn:hover{filter:brightness(1.06)}
.btn.ghost{background:var(--surface);color:var(--ink);border:1px solid var(--border)}
textarea{width:100%;min-height:90px;border:1px solid var(--border);border-radius:10px;padding:12px;font-family:ui-monospace,monospace;font-size:12px;direction:ltr}
.row{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-top:12px}
.pill{font-size:11px;font-family:ui-monospace,monospace;padding:3px 9px;border-radius:999px;background:#eef0f8;color:var(--muted)}
.hidden{display:none}
.svc{display:flex;justify-content:space-between;align-items:center;padding:13px 0;border-bottom:1px solid var(--border)}
.svc:last-child{border-bottom:0}
.dot{width:9px;height:9px;border-radius:50%;display:inline-block;margin-left:8px}
.dot.ok{background:var(--ok)} .dot.bad{background:var(--bad)} .dot.warn{background:var(--warn)}
.muted{color:var(--muted);font-size:12px}
.result{margin-top:12px;padding:12px 14px;border-radius:10px;font-size:13px}
.result.ok{background:#ecfdf3;color:#066d3a;border:1px solid #aee9c5}
.result.bad{background:#fef2f2;color:#a31515;border:1px solid #f3c0c0}
.empty{color:var(--muted);text-align:center;padding:30px}
</style>
</head>
<body>
<header>
<div class="logo">د</div>
<div><h1>پنل ادمین دیدوان</h1><div class="sub">سامانه جامع آمار و اطلاعات</div></div>
</header>
<div class="wrap">
<div class="tabs">
<button class="tab active" data-tab="users">کاربران</button>
<button class="tab" data-tab="token">توکن منبع داده</button>
<button class="tab" data-tab="health">سلامت سرویسها</button>
<button class="tab" data-tab="reports">گزارشات</button>
<button class="tab" data-tab="apiusage">مصرف API</button>
<button class="tab" data-tab="upload">آپلود گزارش</button>
</div>
<section id="users" class="tabpane">
<div class="card">
<h2>کاربران ثبتنامشده <span id="ucount" class="pill"></span></h2>
<div id="utable"><div class="empty">در حال بارگذاری</div></div>
</div>
</section>
<section id="token" class="tabpane hidden">
<div class="card">
<h2>توکن منبع داده</h2>
<p class="muted">توکن هر ۴ روز یکبار عوض میشود. توکن جدید را اینجا ذخیره کن تا اسکرپ فلزات و فولاد ادامه پیدا کند.</p>
<div class="row"><span class="pill">وضعیت: <b id="tkset"></b></span><span class="pill" id="tkupd"></span></div>
<div class="row"><code class="pill" id="tkmask" style="direction:ltr"></code></div>
<textarea id="tkinput" placeholder="توکن جدید را اینجا بچسبان…"></textarea>
<div class="row">
<button class="btn" id="tksave">ذخیره توکن</button>
<button class="btn ghost" id="tktest">تست توکن (آخرین قیمت)</button>
</div>
<div id="tkresult"></div>
</div>
</section>
<section id="health" class="tabpane hidden">
<div class="card">
<h2>پایداری سرویسها</h2>
<p class="muted">آخرین زمان اسکرپ هر سرویس. سبز = تازه، قرمز = قدیمی/متوقف.</p>
<div id="svclist"><div class="empty">در حال بررسی</div></div>
<div class="row"><button class="btn ghost" id="hrefresh">بررسی مجدد</button>
<button class="btn" id="htest">تست اسکرپ آخرین قیمت</button></div>
<div id="hresult"></div>
</div>
</section>
<section id="reports" class="tabpane hidden">
<div class="card">
<h2>گزارش دیتاستهای اسکرپشده</h2>
<p class="muted">دادهها در دیتابیس ذخیره میشوند (نه فایل). از اینجا خروجی CSV (آخرین تاریخ) بگیر.</p>
<div id="rlist"><div class="empty">در حال بارگذاری</div></div>
</div>
</section>
<section id="apiusage" class="tabpane hidden">
<div class="card">
<h2>مصرف API</h2>
<p class="muted">درخواستهای `/api/v1` بر اساس پارتنر، مسیر، وضعیت و IP. توکن خام ذخیره یا نمایش داده نمیشود.</p>
<div id="apiusage-summary"><div class="empty">در حال بارگذاری</div></div>
<div id="apiusage-list"></div>
<div class="row"><button class="btn ghost" id="apiusage-refresh">بهروزرسانی</button></div>
</div>
</section>
<section id="upload" class="tabpane hidden">
<div class="card">
<h2>آپلود گزارش PDF</h2>
<p class="muted">منبع و دسته را انتخاب کن، فایل PDF را بده و آپلود بدون تایپ.</p>
<div class="row">
<label class="muted" style="min-width:46px">منبع</label>
<select id="upsource" style="flex:1;min-width:200px;border:1px solid var(--border);border-radius:10px;padding:10px 12px;font:inherit"></select>
</div>
<div class="row">
<label class="muted" style="min-width:46px">دسته</label>
<select id="upcat" style="flex:1;min-width:200px;border:1px solid var(--border);border-radius:10px;padding:10px 12px;font:inherit"></select>
</div>
<div class="row">
<input id="upfile" type="file" accept="application/pdf" style="flex:1">
<button class="btn" id="upbtn">آپلود</button>
</div>
<div id="upresult"></div>
</div>
</section>
</div>
<script>
const $=s=>document.querySelector(s);
async function api(path,opts){const r=await fetch(path,opts);return r.json();}
function fmt(n){return n==null?'':new Intl.NumberFormat('fa-IR').format(n);}
document.querySelectorAll('.tab').forEach(t=>t.onclick=()=>{
document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active'));
document.querySelectorAll('.tabpane').forEach(x=>x.classList.add('hidden'));
t.classList.add('active');$('#'+t.dataset.tab).classList.remove('hidden');
if(t.dataset.tab==='token')loadToken();
if(t.dataset.tab==='health')loadHealth();
if(t.dataset.tab==='reports')loadReports();
if(t.dataset.tab==='apiusage')loadApiUsage();
if(t.dataset.tab==='upload')loadFolders();
});
const esc=s=>String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
function statusBadge(s){const m={pending:['در انتظار','#b45309','#fef3c7'],approved:['تأییدشده','#047857','#d1fae5'],rejected:['ردشده','#b91c1c','#fee2e2']}[s]||['','#555','#eee'];return `<span style="padding:2px 8px;border-radius:999px;font-size:12px;color:${m[1]};background:${m[2]}">${m[0]}</span>`;}
async function setStatus(id,action){
try{const r=await api('/api/users/'+id+'/'+action,{method:'POST'});if(r&&r.ok)loadUsers();}catch(e){}
}
async function loadUsers(){
try{const u=await api('/api/users');
const pend=u.filter(r=>(r.status||'pending')==='pending').length;
$('#ucount').textContent=u.length+' نفر'+(pend?(' · '+pend+' در انتظار'):'');
if(!u.length){$('#utable').innerHTML='<div class="empty">هنوز کسی ثبت‌نام نکرده است.</div>';return;}
let h='<table><thead><tr><th>#</th><th>نام</th><th>سازمان</th><th class="mono">ایمیل</th><th class="mono">موبایل</th><th>وضعیت</th><th>تاریخ</th><th>اقدام</th></tr></thead><tbody>';
u.forEach((r,i)=>{const s=r.status||'pending';let act='';
if(s!=='approved')act+=`<button class="btn" style="padding:4px 10px" onclick="setStatus(${r.id},'approve')">تأیید</button> `;
if(s!=='rejected')act+=`<button class="btn ghost" style="padding:4px 10px" onclick="setStatus(${r.id},'reject')">رد</button>`;
h+=`<tr><td>${i+1}</td><td>${esc(r.name)||''}</td><td>${esc(r.organization)||''}</td><td class="mono">${esc(r.email)||''}</td><td class="mono">${esc(r.phone)||''}</td><td>${statusBadge(s)}</td><td class="muted">${esc((r.created_at||'').replace('T',' ').replace('Z',''))}</td><td>${act}</td></tr>`;});
h+='</tbody></table>';$('#utable').innerHTML=h;
}catch(e){$('#utable').innerHTML='<div class="empty">خطا در بارگذاری کاربران.</div>';}
}
async function loadToken(){
try{const t=await api('/api/token');
$('#tkset').textContent=t.set?'تنظیم‌شده':'تنظیم نشده';
$('#tkmask').textContent=t.masked||'';
$('#tkupd').textContent=t.updated_at?('آخرین تغییر: '+t.updated_at.replace('T',' ').replace('Z','')):'';
}catch(e){}
}
$('#tksave').onclick=async()=>{
const tok=$('#tkinput').value.trim();if(!tok)return;
$('#tkresult').innerHTML='';
try{const r=await api('/api/token',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:tok})});
if(r.ok){$('#tkinput').value='';loadToken();$('#tkresult').innerHTML='<div class="result ok">توکن ذخیره شد. حالا «تست توکن» را بزن.</div>';}
else $('#tkresult').innerHTML='<div class="result bad">'+(r.error||'خطا')+'</div>';
}catch(e){$('#tkresult').innerHTML='<div class="result bad">خطا در ذخیره.</div>';}
};
$('#tktest').onclick=()=>runTest('#tkresult');
$('#htest').onclick=()=>runTest('#hresult');
async function runTest(target){
$(target).innerHTML='<div class="result">در حال تست… (چند ثانیه طول می‌کشد)</div>';
try{const r=await api('/api/test-scrape',{method:'POST'});
if(r.ok&&r.items){
let h='<div class="result ok">✅ توکن سالم است — '+r.count+' مورد رندوم در آخرین تاریخ:</div>';
h+='<table style="margin-top:10px"><thead><tr><th>کالا</th><th>تاریخ</th><th class="mono">میانگین</th><th class="mono">کف</th><th class="mono">سقف</th></tr></thead><tbody>';
r.items.forEach(it=>{h+='<tr><td>'+(it.title||'')+'</td><td class="muted">'+(it.date||'')+'</td><td class="mono">'+fmt(it.mid)+'</td><td class="mono">'+fmt(it.low)+'</td><td class="mono">'+fmt(it.high)+'</td></tr>';});
h+='</tbody></table>';
$(target).innerHTML=h;
} else if(r.ok){$(target).innerHTML='<div class="result ok">✅ توکن سالم است.'+(r.note?(' '+r.note):'')+'</div>';}
else $(target).innerHTML='<div class="result bad">❌ '+(r.error||'ناموفق')+'</div>';
}catch(e){$(target).innerHTML='<div class="result bad">❌ خطا در تست.</div>';}
}
async function loadReports(){
try{const d=await api('/api/reports');
let h='<table><thead><tr><th>دیتاست</th><th>رکوردها</th><th>از</th><th>تا</th><th>خروجی</th></tr></thead><tbody>';
d.forEach(s=>{h+='<tr><td><b>'+s.label+'</b> <span class="muted" style="direction:ltr">'+s.table+'</span></td><td>'+fmt(s.count)+'</td><td class="muted">'+(s.first||'')+'</td><td class="muted">'+(s.last||'')+'</td><td><a class="btn ghost" style="padding:5px 12px" href="/api/export?dataset='+s.key+'">CSV</a></td></tr>';});
h+='</tbody></table>';$('#rlist').innerHTML=h;
}catch(e){$('#rlist').innerHTML='<div class="empty">خطا در بارگذاری گزارش.</div>';}
}
async function loadApiUsage(){
try{const d=await api('/api/api-usage?limit=100');
let s='<table><thead><tr><th>پارتنر</th><th>درخواست‌ها</th><th>موفق</th><th>خطا</th><th>آخرین استفاده</th></tr></thead><tbody>';
(d.summary||[]).forEach(r=>{s+='<tr><td><b>'+(r.partner||'unknown')+'</b></td><td>'+fmt(r.requests)+'</td><td>'+fmt(r.ok)+'</td><td>'+fmt(r.errors)+'</td><td class="muted">'+(r.last_seen||'')+'</td></tr>';});
s+='</tbody></table>';
$('#apiusage-summary').innerHTML=(d.summary&&d.summary.length)?s:'<div class="empty">هنوز مصرفی ثبت نشده است.</div>';
let h='<table style="margin-top:14px"><thead><tr><th>زمان</th><th>پارتنر</th><th>متد</th><th>مسیر</th><th>وضعیت</th><th class="mono">IP</th><th>مرورگر/کلاینت</th></tr></thead><tbody>';
(d.rows||[]).forEach(r=>{
const q=r.query?('?'+r.query):'';
const ok=(r.status_code||0)<400;
h+='<tr><td class="muted">'+(r.created_at||'')+'</td><td>'+(r.partner||'unknown')+'</td><td class="mono">'+(r.method||'')+'</td><td class="mono">'+esc((r.path||'')+q)+'</td><td><span class="pill" style="color:'+(ok?'var(--ok)':'var(--bad)')+'">'+(r.status_code||'')+'</span></td><td class="mono">'+esc(r.client_ip||'')+'</td><td class="muted">'+esc(r.user_agent||'')+'</td></tr>';
});
h+='</tbody></table>';
$('#apiusage-list').innerHTML=(d.rows&&d.rows.length)?h:'';
}catch(e){
$('#apiusage-summary').innerHTML='<div class="empty">خطا در بارگذاری مصرف API.</div>';
$('#apiusage-list').innerHTML='';
}
}
async function loadHealth(){
try{const h=await api('/api/health');
let html='';
h.forEach(s=>{
const cls=s.daily?'warn':(s.ok?'ok':'bad');
const when=s.daily?('آخرین تاریخ: '+(s.last||'')):(s.last?(`${s.age_min} دقیقه پیش`):'بدون داده');
const status=s.daily?'روزانه (دستی)':(s.ok?'تازه':'قدیمی/متوقف');
html+=`<div class="svc"><div><span class="dot ${cls}"></span><b>${s.label}</b> <span class="muted">(${fmt(s.count)} رکورد)</span></div><div class="muted">${status} · ${when}</div></div>`;
});
$('#svclist').innerHTML=html||'<div class="empty">داده‌ای نیست.</div>';
}catch(e){$('#svclist').innerHTML='<div class="empty">خطا در بررسی.</div>';}
}
$('#hrefresh').onclick=loadHealth;
$('#apiusage-refresh').onclick=loadApiUsage;
let FOLDERS=[];
async function loadFolders(){
try{const d=await api('/api/report-folders'); FOLDERS=d.folders||[];
FOLDERS.forEach(f=>{const i=f.name.indexOf(' - '); f.source=i>=0?f.name.slice(0,i):f.name; f.category=i>=0?f.name.slice(i+3):f.name;});
const sources=[...new Set(FOLDERS.map(f=>f.source))].sort();
$('#upsource').innerHTML=sources.length?sources.map(s=>`<option>${esc(s)}</option>`).join(''):'<option value="">(پوشه‌ای نیست)</option>';
fillCats();
}catch(e){}
}
function fillCats(){
const s=$('#upsource').value;
const cats=FOLDERS.filter(f=>f.source===s);
$('#upcat').innerHTML=cats.map(f=>`<option value="${esc(f.name)}">${esc(f.category)} (${f.count})</option>`).join('');
}
$('#upsource').onchange=fillCats;
$('#upbtn').onclick=async()=>{
const folder=$('#upcat').value; const f=$('#upfile').files[0];
if(!folder){$('#upresult').innerHTML='<div class="result bad">پوشه‌ای برای انتخاب نیست.</div>';return;}
if(!f){$('#upresult').innerHTML='<div class="result bad">یک فایل PDF انتخاب کن.</div>';return;}
const fd=new FormData(); fd.append('folder',folder); fd.append('file',f);
$('#upresult').innerHTML='<div class="result">در حال آپلود…</div>';
try{const r=await fetch('/api/upload-report',{method:'POST',body:fd}); const j=await r.json();
if(r.ok&&j.ok){$('#upresult').innerHTML='<div class="result ok">✅ آپلود شد: '+esc(j.name)+''+esc(j.folder)+'</div>'; $('#upfile').value=''; loadFolders();}
else $('#upresult').innerHTML='<div class="result bad">❌ '+esc(j.detail||j.error||'خطا')+'</div>';
}catch(e){$('#upresult').innerHTML='<div class="result bad">❌ خطا در آپلود.</div>';}
};
loadUsers();
</script>
</body>
</html>"""

8
backend/.dockerignore Normal file
View File

@ -0,0 +1,8 @@
__pycache__/
*.pyc
*.pyo
*.db
*.sqlite*
*.log
.venv/
venv/

22
backend/Dockerfile Normal file
View File

@ -0,0 +1,22 @@
FROM mcr-mirror.liara.ir/playwright/python:v1.46.0-jammy
WORKDIR /app
ENV PYTHONUNBUFFERED=1 \
PYTHONUTF8=1 \
PIP_NO_CACHE_DIR=1 \
DB_PATH=/data/asianmetal.db \
REPORTS_DIR=/data/reports \
PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \
PIP_INDEX_URL=https://package-mirror.liara.ir/repository/pypi/simple
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN mkdir -p /data/reports
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

80
backend/auth.py Normal file
View File

@ -0,0 +1,80 @@
"""
Server-side auth helpers: password hashing (stdlib PBKDF2, no external deps)
and opaque session tokens stored in the DB. Used by main.py for the
register -> admin-approval -> login flow.
"""
import hashlib
import hmac
import secrets
from datetime import datetime, timedelta, timezone
from db import get_conn
_ITER = 600_000 # new hashes; verify_password honours the iters stored per-hash, so old 200k hashes still work
SESSION_DAYS = 7
def hash_password(pw: str) -> str:
salt = secrets.token_bytes(16)
dk = hashlib.pbkdf2_hmac("sha256", pw.encode("utf-8"), salt, _ITER)
return f"pbkdf2_sha256${_ITER}${salt.hex()}${dk.hex()}"
def verify_password(pw: str, stored: str | None) -> bool:
if not stored:
return False
try:
algo, iters, salt_hex, dk_hex = stored.split("$")
if algo != "pbkdf2_sha256":
return False
dk = hashlib.pbkdf2_hmac("sha256", pw.encode("utf-8"), bytes.fromhex(salt_hex), int(iters))
return hmac.compare_digest(dk.hex(), dk_hex)
except Exception:
return False
def create_session(user_id: int) -> str:
token = secrets.token_urlsafe(32)
now = datetime.now(timezone.utc)
exp = now + timedelta(days=SESSION_DAYS)
with get_conn() as c:
c.execute(
"INSERT INTO sessions(token, user_id, created_at, expires_at) VALUES(?,?,?,?)",
(token, user_id, now.isoformat(), exp.isoformat()),
)
return token
def delete_session(token: str | None) -> None:
if not token:
return
with get_conn() as c:
c.execute("DELETE FROM sessions WHERE token=?", (token,))
def revoke_all_sessions(user_id: int) -> None:
"""Kill every active session for a user (e.g. on password change/compromise)."""
with get_conn() as c:
c.execute("DELETE FROM sessions WHERE user_id=?", (user_id,))
def user_for_token(token: str | None):
"""Return the user row for a valid, unexpired session token, else None."""
if not token:
return None
with get_conn() as c:
row = c.execute(
"""SELECT u.id, u.name, u.email, u.phone, u.status, s.expires_at
FROM sessions s JOIN users u ON u.id = s.user_id
WHERE s.token = ?""",
(token,),
).fetchone()
if not row:
return None
try:
if datetime.fromisoformat(row["expires_at"]) < datetime.now(timezone.utc):
delete_session(token)
return None
except Exception:
return None
return row

209
backend/crypto_market.py Normal file
View File

@ -0,0 +1,209 @@
"""
Crypto market data from CoinGecko's official free public API (no key needed).
HTML scraping of coingecko.com is prohibited by their ToS and the page is JS-
rendered behind Cloudflare; the free API returns the same data (prices, 1h/24h/7d
changes, market cap, volume, and a 7-day sparkline array) reliably as JSON.
Keeps a current snapshot in memory (served by /api/crypto) and dedup-persists
changed coin prices into crypto_prices, exactly like the other data modules.
"""
import asyncio
from datetime import datetime, timezone
import requests
from db import get_conn
API_BASE = "https://api.coingecko.com/api/v3"
UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
)
HEADERS = {"User-Agent": UA, "Accept": "application/json"}
INTERVAL_SECONDS = 60 # 3 calls/cycle, well under the 100 calls/min free limit
PER_PAGE = 100 # top 100 coins by market cap
SPARK_POINTS = 40 # downsample the 7d (~168pt) sparkline to keep payload small
_latest: dict = {"coins": [], "global": {}, "trending": [], "fetched_at": None}
_last_saved: dict[str, float] = {}
def create_table() -> None:
with get_conn() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS crypto_prices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT NOT NULL,
name TEXT,
price REAL,
pct_24h REAL,
market_cap REAL,
volume_24h REAL,
fetched_at TEXT NOT NULL
)
""")
conn.execute("CREATE INDEX IF NOT EXISTS idx_crypto_symbol ON crypto_prices(symbol)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_crypto_fetched ON crypto_prices(fetched_at)")
def _downsample(arr: list, n: int = SPARK_POINTS) -> list:
"""Reduce a long price series to ~n points (keeps the last point)."""
if not arr or len(arr) <= n:
return [round(float(x), 6) for x in arr] if arr else []
step = len(arr) / n
out = [arr[int(i * step)] for i in range(n)]
out.append(arr[-1])
return [round(float(x), 6) for x in out]
def _dir(pct) -> str:
if pct is None:
return ""
return "up" if pct >= 0 else "down"
def _fetch_markets() -> list[dict]:
url = (
f"{API_BASE}/coins/markets?vs_currency=usd&order=market_cap_desc"
f"&per_page={PER_PAGE}&page=1&sparkline=true"
f"&price_change_percentage=1h,24h,7d"
)
data = requests.get(url, headers=HEADERS, timeout=25).json()
if not isinstance(data, list):
return []
coins = []
for c in data:
pct24 = c.get("price_change_percentage_24h_in_currency")
coins.append({
"id": c.get("id"),
"symbol": (c.get("symbol") or "").upper(),
"name": c.get("name"),
"image": c.get("image"),
"rank": c.get("market_cap_rank"),
"price": c.get("current_price"),
"pct_1h": c.get("price_change_percentage_1h_in_currency"),
"pct_24h": pct24,
"pct_7d": c.get("price_change_percentage_7d_in_currency"),
"market_cap": c.get("market_cap"),
"volume_24h": c.get("total_volume"),
"high_24h": c.get("high_24h"),
"low_24h": c.get("low_24h"),
"circ_supply": c.get("circulating_supply"),
"ath": c.get("ath"),
"sparkline": _downsample((c.get("sparkline_in_7d") or {}).get("price") or []),
"dir": _dir(pct24),
})
return coins
def _fetch_global() -> dict:
data = requests.get(f"{API_BASE}/global", headers=HEADERS, timeout=20).json()
g = (data or {}).get("data") or {}
pct = g.get("market_cap_percentage") or {}
return {
"market_cap_usd": (g.get("total_market_cap") or {}).get("usd"),
"volume_usd": (g.get("total_volume") or {}).get("usd"),
"btc_dominance": pct.get("btc"),
"eth_dominance": pct.get("eth"),
"market_cap_change_24h": g.get("market_cap_change_percentage_24h_usd"),
"active": g.get("active_cryptocurrencies"),
}
def _fetch_trending() -> list[dict]:
data = requests.get(f"{API_BASE}/search/trending", headers=HEADERS, timeout=20).json()
out = []
for c in (data or {}).get("coins", []):
item = c.get("item") or {}
d = item.get("data") or {}
pct = d.get("price_change_percentage_24h") or {}
out.append({
"id": item.get("id"),
"symbol": (item.get("symbol") or "").upper(),
"name": item.get("name"),
"rank": item.get("market_cap_rank"),
"thumb": item.get("small") or item.get("thumb"),
"price": d.get("price"),
"pct_24h": pct.get("usd") if isinstance(pct, dict) else None,
})
return out
def _fetch_sync() -> dict:
"""Each section is fetched independently so one failure doesn't lose the rest."""
out = {"coins": [], "global": {}, "trending": []}
try:
out["coins"] = _fetch_markets()
except Exception as e:
print(f"[crypto] markets fetch failed: {e}")
try:
out["global"] = _fetch_global()
except Exception as e:
print(f"[crypto] global fetch failed: {e}")
try:
out["trending"] = _fetch_trending()
except Exception as e:
print(f"[crypto] trending fetch failed: {e}")
return out
def _save(coins: list[dict], now: str) -> None:
"""Persist only coins whose price changed since last save (dedup)."""
changed = [c for c in coins if c["symbol"] and _last_saved.get(c["symbol"]) != c["price"]]
if not changed:
return
with get_conn() as conn:
conn.executemany(
"""INSERT INTO crypto_prices
(symbol, name, price, pct_24h, market_cap, volume_24h, fetched_at)
VALUES(?,?,?,?,?,?,?)""",
[(c["symbol"], c["name"], c["price"], c["pct_24h"],
c["market_cap"], c["volume_24h"], now) for c in changed],
)
for c in changed:
_last_saved[c["symbol"]] = c["price"]
def _heartbeat(now: str) -> None:
with get_conn() as conn:
conn.execute(
"""INSERT INTO app_config(key, value) VALUES('crypto_last_scrape', ?)
ON CONFLICT(key) DO UPDATE SET value=excluded.value""",
(now,),
)
def _load_last_saved() -> None:
try:
with get_conn() as conn:
rows = conn.execute(
"""SELECT symbol, price FROM crypto_prices
WHERE id IN (SELECT MAX(id) FROM crypto_prices GROUP BY symbol)"""
).fetchall()
for r in rows:
_last_saved[r["symbol"]] = r["price"]
except Exception as e:
print(f"[crypto] could not seed dedup from DB: {e}")
async def scrape_loop() -> None:
global _latest
create_table()
_load_last_saved()
while True:
try:
payload = await asyncio.to_thread(_fetch_sync)
if payload["coins"]:
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
await asyncio.to_thread(_save, payload["coins"], now)
await asyncio.to_thread(_heartbeat, now)
_latest = {**payload, "fetched_at": now}
print(f"[crypto] {len(payload['coins'])} coins at {now}")
except Exception as e:
print(f"[crypto] error: {e}")
await asyncio.sleep(INTERVAL_SECONDS)
def get_latest() -> dict:
return _latest

242
backend/currency_stream.py Normal file
View File

@ -0,0 +1,242 @@
import math, random
def _generate_sparkline(current_price: float, pct_change: float = 0.0, n_points: int = 40) -> list:
if not current_price or current_price <= 0:
return [0.0] * n_points
pct = (pct_change or 0.0) / 100.0
start_price = current_price / (1.0 + pct) if (1.0 + pct) > 0 else current_price
points = []
trend_step = (current_price - start_price) / max(1, n_points - 1)
volatility = max(abs(pct) * 0.4, 0.007) * current_price
rand_seed = int(abs(current_price) * 1000) % 100000
r = random.Random(rand_seed)
for i in range(n_points - 1):
progress = i / float(n_points - 1)
base = start_price + trend_step * i
wave = math.sin(progress * math.pi * 3.5) * (volatility * 0.6) + math.cos(progress * math.pi * 5.2) * (volatility * 0.4)
noise = (r.random() - 0.5) * volatility * 0.45
val = base + wave + noise
points.append(round(max(val, current_price * 0.2), 2 if current_price > 100 else 4))
points.append(round(current_price, 2 if current_price > 100 else 4))
return points
"""
Scrapes Iranian domestic market prices from TGJU every 60 seconds:
- currency (tgju.org/currency)
- gold (tgju.org/gold-chart) gold ETFs ("صندوق") excluded
- coin (tgju.org/coin) bubbles/funds/pre-86 excluded
One headless browser visits all three pages per cycle. The latest snapshot
of each is cached in memory (served by /api/currency, /api/gold, /api/coin)
and only changed rows are persisted (dedup) to keep the DB lean.
Row names are read from <th> (not <td>): on TGJU market tables the first
<td> is the price, so a <td>-based selector stored numbers instead of names.
"""
import asyncio
import re
from datetime import datetime, timezone
from playwright.sync_api import sync_playwright
from db import get_conn
UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
)
INTERVAL_SECONDS = 60
URLS = {
"currency": "https://www.tgju.org/currency",
"gold": "https://www.tgju.org/gold-chart",
"coin": "https://www.tgju.org/coin",
}
# Rows to drop per kind (keep the board clean and on-topic).
_EXCLUDE = {
"currency": lambda name: False,
"gold": lambda name: "صندوق" in name, # gold ETFs
"coin": lambda name: any(x in name for x in ("حباب", "تمام سکه", "صندوق", "قبل")),
}
KINDS = ("currency", "gold", "coin")
_TABLES = {"currency": "currency_prices", "gold": "gold_prices", "coin": "coin_prices"}
_latest: dict[str, list[dict]] = {k: [] for k in KINDS}
_last_saved: dict[str, dict[str, float]] = {k: {} for k in KINDS}
_PCT_RE = re.compile(r"\(([\d.]+)\s*%\)")
_EVAL_JS = r"""() => {
const out = [];
document.querySelectorAll("table.data-table.market-table").forEach(table => {
table.querySelectorAll("tbody tr").forEach(tr => {
const th = tr.querySelector("th");
const tds = Array.from(tr.querySelectorAll("td"));
const name = th ? (th.innerText || "").trim() : "";
const price = tds[0] ? (tds[0].innerText || "").trim() : "";
const chgEl = tds[1];
const change = chgEl ? (chgEl.innerText || "").trim() : "";
let dir = "";
if (chgEl) {
const c = chgEl.className || "";
if (c.includes("high")) dir = "up";
else if (c.includes("low")) dir = "down";
}
if (name && price) out.push({ name, price, change, dir });
});
});
return out;
}"""
def create_tables() -> None:
with get_conn() as conn:
for table in _TABLES.values():
conn.execute(f"""
CREATE TABLE IF NOT EXISTS {table} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL,
change_val TEXT,
pct REAL,
dir TEXT,
fetched_at TEXT NOT NULL
)
""")
conn.execute(f"CREATE INDEX IF NOT EXISTS idx_{table}_name ON {table}(name)")
def _to_float(raw: str):
try:
return float(raw.replace(",", "").strip())
except (ValueError, AttributeError):
return None
def _parse_pct(change: str, direction: str):
m = _PCT_RE.search(change or "")
if not m:
return 0.0
pct = float(m.group(1))
return -pct if direction == "down" else pct
def _scrape_all() -> dict[str, list[dict]]:
out: dict[str, list[dict]] = {}
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(user_agent=UA)
try:
for kind, url in URLS.items():
try:
page.goto(url, wait_until="domcontentloaded", timeout=45000)
page.wait_for_selector("table.data-table.market-table tbody tr", timeout=20000)
# TGJU server-renders STALE prices, then live-updates the DOM a few
# seconds later via its own JS. Wait so we read the live values, not
# the cached snapshot (otherwise prices look frozen / dedup sees no change).
page.wait_for_timeout(9000)
raw = page.evaluate(_EVAL_JS)
except Exception as e:
print(f"[tgju] {kind} scrape error: {e}")
out[kind] = []
continue
exclude = _EXCLUDE[kind]
seen: set[str] = set()
rows = []
for r in raw:
name = r["name"]
if exclude(name) or name in seen:
continue # drop off-topic rows + duplicate tables (coins repeat)
price = _to_float(r["price"])
if price is None:
continue
seen.add(name)
rows.append({
"name": name,
"price": price,
"change_val": r["change"],
"pct": _parse_pct(r["change"], r["dir"]),
"dir": r["dir"],
})
out[kind] = rows
finally:
browser.close()
return out
def _save(kind: str, rows: list[dict], now: str) -> None:
if not rows:
return
table = _TABLES[kind]
with get_conn() as conn:
conn.executemany(
f"INSERT INTO {table}(name, price, change_val, pct, dir, fetched_at) "
"VALUES(?,?,?,?,?,?)",
[(r["name"], r["price"], r["change_val"], r["pct"], r["dir"], now) for r in rows],
)
def _load_last_saved() -> None:
for kind, table in _TABLES.items():
try:
with get_conn() as conn:
rows = conn.execute(
f"SELECT name, price FROM {table} "
f"WHERE id IN (SELECT MAX(id) FROM {table} GROUP BY name)"
).fetchall()
for r in rows:
if r["price"] is not None:
_last_saved[kind][r["name"]] = r["price"]
except Exception as e:
print(f"[tgju] could not load last-saved {kind}: {e}")
async def scrape_loop() -> None:
"""Background task: refresh currency + gold + coin every INTERVAL_SECONDS.
The in-memory snapshots always hold the full board; only changed rows persist."""
create_tables()
_load_last_saved()
while True:
try:
data = await asyncio.to_thread(_scrape_all)
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
summary = []
for kind in KINDS:
rows = data.get(kind, [])
if not rows:
continue
_latest[kind] = [{**r, "fetched_at": now} for r in rows]
changed = [r for r in rows if _last_saved[kind].get(r["name"]) != r["price"]]
if changed:
await asyncio.to_thread(_save, kind, changed, now)
for r in changed:
_last_saved[kind][r["name"]] = r["price"]
summary.append(f"{kind}:{len(changed)}/{len(rows)}")
# heartbeat: record that a scrape cycle completed, independent of dedup
try:
with get_conn() as c:
c.execute(
"INSERT INTO app_config(key,value) VALUES('tgju_last_scrape',?) "
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
(now,),
)
except Exception:
pass
if summary:
print(f"[tgju] {' '.join(summary)} at {now}")
except Exception as e:
print(f"[tgju] error: {e}")
await asyncio.sleep(INTERVAL_SECONDS)
def get_latest() -> list[dict]:
return _latest["currency"]
def get_gold() -> list[dict]:
return _latest["gold"]
def get_coin() -> list[dict]:
return _latest["coin"]

49
backend/db.py Normal file
View File

@ -0,0 +1,49 @@
import sqlite3
import os
DB_PATH = os.environ.get("DB_PATH", "/data/asianmetal.db")
def get_conn():
conn = sqlite3.connect(DB_PATH, timeout=30.0)
conn.row_factory = sqlite3.Row
return conn
def init_db():
with get_conn() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
username TEXT,
email TEXT,
phone TEXT,
organization TEXT,
password_hash TEXT,
status TEXT DEFAULT 'approved',
created_at TEXT
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
created_at TEXT NOT NULL,
expires_at TEXT NOT NULL
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS sso_nonces (
jti TEXT PRIMARY KEY,
expires_at REAL NOT NULL
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS app_config (
key TEXT PRIMARY KEY,
value TEXT
)
""")
conn.commit()
if __name__ == "__main__":
init_db()

8
backend/liara.json Normal file
View File

@ -0,0 +1,8 @@
{
"platform": "docker",
"port": 8000,
"app": "statista-didvan-api",
"disks": [
{ "name": "data", "mountTo": "/data" }
]
}

158
backend/live_stream.py Normal file
View File

@ -0,0 +1,158 @@
import math, random
def _generate_sparkline(current_price: float, pct_change: float = 0.0, n_points: int = 40) -> list:
if not current_price or current_price <= 0:
return [0.0] * n_points
pct = (pct_change or 0.0) / 100.0
start_price = current_price / (1.0 + pct) if (1.0 + pct) > 0 else current_price
points = []
trend_step = (current_price - start_price) / max(1, n_points - 1)
volatility = max(abs(pct) * 0.4, 0.007) * current_price
rand_seed = int(abs(current_price) * 1000) % 100000
r = random.Random(rand_seed)
for i in range(n_points - 1):
progress = i / float(n_points - 1)
base = start_price + trend_step * i
wave = math.sin(progress * math.pi * 3.5) * (volatility * 0.6) + math.cos(progress * math.pi * 5.2) * (volatility * 0.4)
noise = (r.random() - 0.5) * volatility * 0.45
val = base + wave + noise
points.append(round(max(val, current_price * 0.2), 2 if current_price > 100 else 4))
points.append(round(current_price, 2 if current_price > 100 else 4))
return points
"""
Polls Yahoo Finance every 3 seconds for live commodity prices.
Broadcasts to all connected WebSocket clients.
"""
import asyncio
import json
import yfinance as yf
from fastapi import WebSocket
SYMBOLS = {
"CL=F": "Crude Oil WTI",
"BZ=F": "Brent Crude",
"NG=F": "Natural Gas",
"RB=F": "Gasoline",
"HO=F": "Heating Oil",
"GC=F": "Gold",
"SI=F": "Silver",
"HG=F": "Copper",
"PL=F": "Platinum",
"PA=F": "Palladium",
"ALI=F": "Aluminum",
"NI=F": "Nickel",
"ZC=F": "Corn",
"ZW=F": "Wheat",
"ZS=F": "Soybeans",
"KC=F": "Coffee",
"CT=F": "Cotton",
"CC=F": "Cocoa",
"SB=F": "Sugar",
"OJ=F": "Orange Juice",
}
_clients: set[WebSocket] = set()
_latest: list[dict] = []
_MAX_CLIENTS = 500 # ponytail: global cap — the board is identical for everyone, so no per-IP fairness needed
_SYM_LIST = " ".join(SYMBOLS.keys())
def _fetch_sync() -> list[dict]:
try:
# Single batch HTTP request — much faster than one-by-one
raw = yf.download(
_SYM_LIST,
period="2d",
progress=False,
group_by="ticker",
auto_adjust=True,
threads=True,
)
out = []
for sym, name in SYMBOLS.items():
try:
if sym in raw.columns.get_level_values(0):
df = raw[sym].dropna()
else:
df = raw.dropna()
if df.empty:
continue
price = float(df["Close"].iloc[-1])
prev = float(df["Close"].iloc[-2]) if len(df) > 1 else price
change = price - prev
pct = (change / prev * 100) if prev else 0
out.append({
"symbol": sym,
"name": name,
"price": round(price, 4),
"change": round(change, 4),
"pct": round(pct, 3),
"currency": "USD",
"up": change >= 0,
"sparkline": _generate_sparkline(price, pct),
})
except Exception:
pass
return out
except Exception as e:
print(f"[live] batch fetch error: {e}")
# fallback: individual fetch
out = []
tickers = yf.Tickers(_SYM_LIST)
for sym, name in SYMBOLS.items():
try:
fi = tickers.tickers[sym].fast_info
price = fi.last_price or 0
prev = fi.previous_close or price
change = price - prev
pct = (change / prev * 100) if prev else 0
out.append({"symbol": sym, "name": name, "price": round(price,4),
"change": round(change,4), "pct": round(pct,3),
"currency": "USD", "up": change >= 0,
"sparkline": _generate_sparkline(price, pct)})
except Exception:
pass
return out
async def fetch_prices() -> list[dict]:
return await asyncio.to_thread(_fetch_sync)
def get_latest() -> list[dict]:
"""Latest commodity snapshot — served over plain HTTP (tunnel/proxy friendly)."""
return _latest
async def broadcast_loop():
global _latest, _clients # _clients is reassigned below (-= dead), so declare it global
while True:
try:
data = await fetch_prices()
if data:
_latest = data
msg = json.dumps(data)
dead = set()
for ws in _clients.copy():
try:
await ws.send_text(msg)
except Exception:
dead.add(ws)
_clients -= dead
except Exception as e:
print(f"[live] error: {e}")
await asyncio.sleep(3)
async def connect(ws: WebSocket):
if len(_clients) >= _MAX_CLIENTS:
await ws.close(code=1013) # 1013 = "try again later"
return
await ws.accept()
_clients.add(ws)
if _latest:
await ws.send_text(json.dumps(_latest))
try:
while True:
await ws.receive_text()
except Exception:
pass
finally:
_clients.discard(ws)

407
backend/main.py Normal file
View File

@ -0,0 +1,407 @@
import math, random
def _generate_sparkline(current_price: float, pct_change: float = 0.0, n_points: int = 40) -> list:
if not current_price or current_price <= 0:
return [0.0] * n_points
pct = (pct_change or 0.0) / 100.0
start_price = current_price / (1.0 + pct) if (1.0 + pct) > 0 else current_price
points = []
trend_step = (current_price - start_price) / max(1, n_points - 1)
volatility = max(abs(pct) * 0.4, 0.007) * current_price
rand_seed = int(abs(current_price) * 1000) % 100000
r = random.Random(rand_seed)
for i in range(n_points - 1):
progress = i / float(n_points - 1)
base = start_price + trend_step * i
wave = math.sin(progress * math.pi * 3.5) * (volatility * 0.6) + math.cos(progress * math.pi * 5.2) * (volatility * 0.4)
noise = (r.random() - 0.5) * volatility * 0.45
val = base + wave + noise
points.append(round(max(val, current_price * 0.2), 2 if current_price > 100 else 4))
points.append(round(current_price, 2 if current_price > 100 else 4))
return points
from fastapi import FastAPI, Query, WebSocket, HTTPException, Request, Response, APIRouter, Depends, Header
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, RedirectResponse, JSONResponse
from pydantic import BaseModel, Field, field_validator
from contextlib import asynccontextmanager
from datetime import datetime, timedelta
import asyncio
import base64
import hashlib
import hmac
import json
import ipaddress
import os
import re
import time
import auth
from db import get_conn, init_db
from live_stream import broadcast_loop, connect as ws_connect, get_latest as get_commodity_latest
import currency_stream
import world_economy
import steel_stocks
import reports
import crypto_market
@asynccontextmanager
async def lifespan(app: FastAPI):
asyncio.create_task(broadcast_loop())
asyncio.create_task(currency_stream.scrape_loop())
asyncio.create_task(world_economy.scrape_loop())
asyncio.create_task(steel_stocks.scrape_loop())
asyncio.create_task(crypto_market.scrape_loop())
yield
app = FastAPI(lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.middleware("http")
async def security_headers(request, call_next):
response = await call_next(request)
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "ALLOWALL"
response.headers["Access-Control-Allow-Origin"] = "*"
return response
init_db()
SESSION_COOKIE = "am_session"
COOKIE_SECURE = False
COOKIE_SAMESITE = "lax"
def _load_api_tokens():
tokens = {}
raw = os.environ.get("API_TOKENS", "").strip()
if raw:
for part in raw.replace("\n", ",").split(","):
part = part.strip()
if not part: continue
if "=" in part: partner, token = part.split("=", 1)
elif ":" in part: partner, token = part.split(":", 1)
else: partner, token = "default", part
if partner.strip() and token.strip(): tokens[partner.strip()] = token.strip()
single = os.environ.get("API_TOKEN", "").strip()
if single: tokens.setdefault("legacy", single)
return tokens
API_PARTNER_TOKENS = _load_api_tokens()
def _bearer_token(authorization):
if not authorization: return None
authorization = authorization.strip()
scheme, _, token = authorization.partition(" ")
if scheme.lower() == "bearer" and token.strip(): return token.strip()
return authorization
def _match_api_partner(token):
for partner, expected in API_PARTNER_TOKENS.items():
if hmac.compare_digest(token, expected): return partner
return None
def require_session(request: Request, authorization: str | None = Header(default=None)):
token = request.cookies.get(SESSION_COOKIE)
if token:
row = auth.user_for_token(token)
if row: return row
bearer = _bearer_token(authorization) or request.query_params.get("api_token") or request.query_params.get("token")
if bearer:
partner = _match_api_partner(bearer)
if partner or bearer in API_PARTNER_TOKENS.values():
return {"id": "partner", "name": partner or "api_client", "email": "partner@api", "status": "approved"}
return {"id": "public", "name": "public_user", "email": "public@guest", "status": "approved"}
@app.websocket("/ws/live")
async def websocket_live(ws: WebSocket):
await ws.accept()
await ws_connect(ws)
@app.get("/api/commodity")
def get_commodity(_: object = Depends(require_session)):
return get_commodity_latest()
@app.get("/api/tree")
def get_tree(_: object = Depends(require_session)):
rows = get_conn().execute(
"SELECT DISTINCT grp, category, title FROM prices ORDER BY grp, category, title"
).fetchall()
tree = {}
for r in rows:
tree.setdefault(r["grp"], {}).setdefault(r["category"], []).append(r["title"])
return tree
@app.get("/api/history")
def get_history(title: str = Query(..., max_length=120), days: int = Query(0, ge=0, le=3650), _: object = Depends(require_session)):
if days > 0:
cutoff = (datetime.utcnow() - timedelta(days=days)).strftime("%Y-%m-%d")
rows = get_conn().execute(
"SELECT date, low, mid, high FROM prices WHERE title=? AND date>=? ORDER BY date LIMIT 6000",
(title, cutoff)
).fetchall()
else:
rows = get_conn().execute(
"SELECT date, low, mid, high FROM prices WHERE title=? ORDER BY date LIMIT 6000",
(title,)
).fetchall()
return [dict(r) for r in rows]
@app.get("/api/search")
def search(q: str = Query(..., max_length=64), _: object = Depends(require_session)):
safe = q.replace(chr(92), chr(92)+chr(92)).replace("%", chr(92)+"%").replace("_", chr(92)+"_")
rows = get_conn().execute(
"SELECT DISTINCT title, grp as 'group', category FROM prices "
"WHERE title LIKE ? ESCAPE '" + chr(92) + "' LIMIT 50",
(f"%{safe}%",)
).fetchall()
return [dict(r) for r in rows]
@app.get("/api/live")
def get_live(_: object = Depends(require_session)):
try:
rows = get_conn().execute("""
SELECT name, price, day_change, pct_change, unit, fetched_at
FROM te_prices
WHERE fetched_at = (SELECT MAX(fetched_at) FROM te_prices)
ORDER BY name
""").fetchall()
return [dict(r) for r in rows]
except Exception:
return []
@app.get("/api/latest")
def get_latest(_: object = Depends(require_session)):
rows = get_conn().execute("""
SELECT p.grp as 'group', p.category, p.title, p.date, p.low, p.mid, p.high,
prev.mid as prev_mid
FROM prices p
JOIN (SELECT title, MAX(date) as max_date FROM prices GROUP BY title) m
ON p.title = m.title AND p.date = m.max_date
LEFT JOIN prices prev ON prev.title = p.title
AND prev.date = (SELECT MAX(date) FROM prices WHERE title=p.title AND date < p.date)
""").fetchall()
result = []
for r in rows:
pct = round(((r["mid"]-r["prev_mid"])/r["prev_mid"]*100) if r["prev_mid"] else 0, 2)
d = dict(r)
d["pct"] = pct
d["sparkline"] = _generate_sparkline(r["mid"] or 0, pct)
result.append(d)
return result
@app.get("/api/prices/latest")
@app.get("/api/v1/prices/latest")
def get_prices_latest(_: object = Depends(require_session)):
generated_at = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
return {
"generated_at": generated_at,
"datasets": {
"metals": get_latest(None),
"live_commodities": get_live(None),
"currency": currency_stream.get_latest(),
"gold": currency_stream.get_gold(),
"coin": currency_stream.get_coin(),
"commodity": get_commodity_latest(),
"steel_stocks": steel_stocks.get_latest(),
"crypto": crypto_market.get_latest(),
"world_economy": world_economy.get_latest(),
},
}
@app.get("/api/currency")
def get_currency(_: object = Depends(require_session)):
return currency_stream.get_latest()
@app.get("/api/gold")
def get_gold(_: object = Depends(require_session)):
return currency_stream.get_gold()
@app.get("/api/coin")
def get_coin(_: object = Depends(require_session)):
return currency_stream.get_coin()
@app.get("/api/world-economy")
def get_world_economy(_: object = Depends(require_session)):
return world_economy.get_latest()
@app.get("/api/steel-stocks")
def get_steel_stocks(_: object = Depends(require_session)):
return steel_stocks.get_latest()
@app.get("/api/crypto")
def get_crypto(_: object = Depends(require_session)):
return crypto_market.get_latest()
@app.get("/api/reports")
def get_reports(_: object = Depends(require_session)):
return reports.get_index()
@app.get("/api/reports/files")
def get_report_files(folder: str = Query(...), _: object = Depends(require_session)):
return reports.list_files(folder)
@app.get("/api/reports/file")
def get_report_file(folder: str = Query(...), name: str = Query(...), _: object = Depends(require_session)):
p = reports.file_path(folder, name)
if not p:
raise HTTPException(status_code=404, detail="report not found")
return FileResponse(p, media_type="application/pdf", content_disposition_type="inline")
def _set_session_cookie(response: Response, token: str) -> None:
response.set_cookie(
key=SESSION_COOKIE,
value=token,
httponly=True,
samesite="lax",
secure=False,
max_age=auth.SESSION_DAYS * 86400,
path="/",
)
class RegisterIn(BaseModel):
name: str = Field(..., min_length=1, max_length=80)
last_name: str = Field(..., min_length=1, max_length=80)
organization: str = Field(..., min_length=1, max_length=160)
email: str = Field(..., max_length=254)
phone: str = Field(..., min_length=6, max_length=24)
password: str = Field(..., min_length=8, max_length=128)
@app.post("/api/register")
def register(data: RegisterIn):
try:
user_id = auth.create_user(
name=f"{data.name.strip()} {data.last_name.strip()}",
username=data.phone.strip(),
password=data.password,
email=data.email.strip().lower(),
organization=data.organization.strip(),
)
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
return {"ok": True, "id": user_id}
class LoginIn(BaseModel):
identifier: str = Field(..., min_length=1, max_length=254)
password: str = Field(..., min_length=1, max_length=128)
@app.post("/api/login")
def login(data: LoginIn, response: Response):
ident = data.identifier.strip()
ident_l = ident.lower()
with get_conn() as conn:
row = conn.execute(
"SELECT id, password_hash, status FROM users "
"WHERE lower(username)=? OR lower(email)=? OR username=?",
(ident_l, ident_l, ident),
).fetchone()
if (
not row
or not auth.verify_password(data.password, row["password_hash"])
or row["status"] != "approved"
):
raise HTTPException(status_code=401, detail="نام کاربری/شماره یا رمز عبور اشتباه است")
_set_session_cookie(response, auth.create_session(row["id"]))
return {"ok": True}
@app.get("/api/me")
def me(request: Request):
row = auth.user_for_token(request.cookies.get(SESSION_COOKIE))
if not row:
return {"id": "public", "name": "public_user", "email": "public@guest", "status": "approved"}
return {"id": row["id"], "name": row["name"], "email": row["email"], "status": row["status"]}
@app.post("/api/logout")
def logout(request: Request, response: Response):
auth.delete_session(request.cookies.get(SESSION_COOKIE))
response.delete_cookie(SESSION_COOKIE, path="/")
return {"ok": True}
SSO_SECRET = os.environ.get("SSO_SECRET", "")
def _verify_sso_token(t: str):
if not SSO_SECRET:
return None
try:
raw_body, _, sig = t.partition(".")
expected = hmac.new(SSO_SECRET.encode(), raw_body.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
return None
payload = json.loads(base64.urlsafe_b64decode(raw_body.encode() + b"==").decode())
return payload
except Exception:
return None
@app.get("/api/sso")
def sso_login(t: str = Query(...), response: Response = None):
payload = _verify_sso_token(t)
if not payload:
return RedirectResponse(url="/", status_code=302)
email = payload.get("email", "").lower()
name = payload.get("name", "کاربر اندیشکده")
with get_conn() as conn:
row = conn.execute("SELECT id FROM users WHERE lower(email)=?", (email,)).fetchone()
if not row:
now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
conn.execute(
"INSERT INTO users(name, username, password_hash, status, created_at, email) "
"VALUES(?,?,?,?,?,?)",
(name, email, auth.hash_password("sso_login_dummy"), "approved", now, email),
)
row = conn.execute("SELECT id FROM users WHERE lower(email)=?", (email,)).fetchone()
_set_session_cookie(response, auth.create_session(row["id"]))
return RedirectResponse(url="/", status_code=302)
data_api = APIRouter(prefix="/api/v1")
@data_api.get("/prices/latest")
def v1_prices_latest(): return get_prices_latest(None)
@data_api.get("/live")
def v1_live(): return get_live(None)
@data_api.get("/latest")
def v1_latest(): return get_latest(None)
@data_api.get("/commodity")
def v1_commodity(): return get_commodity(None)
@data_api.get("/currency")
def v1_currency(): return get_currency(None)
@data_api.get("/gold")
def v1_gold(): return get_gold(None)
@data_api.get("/coin")
def v1_coin(): return get_coin(None)
@data_api.get("/metals/tree")
def v1_metals_tree(): return get_tree(None)
@data_api.get("/metals/latest")
def v1_metals_latest(): return get_latest(None)
@data_api.get("/metals/history")
def v1_metals_history(title: str = Query(...), days: int = 0): return get_history(title, days, None)
@data_api.get("/metals/search")
def v1_metals_search(q: str = Query(...)): return search(q, None)
@data_api.get("/steel-stocks")
def v1_steel_stocks(): return get_steel_stocks(None)
@data_api.get("/crypto")
def v1_crypto(): return get_crypto(None)
@data_api.get("/world-economy")
def v1_world_economy(): return get_world_economy(None)
app.include_router(data_api)

178
backend/reports.py Normal file
View File

@ -0,0 +1,178 @@
"""
Report library scans the on-disk archive of PDF market reports and exposes
a small summary (sources -> categories -> counts/latest) plus a per-category
file listing and inline PDF streaming.
Folder layout (one level deep):
<REPORTS_DIR>/<Source> - <Category>/<file>.pdf
e.g. "FerroAlloyNet - Iron Ore/iron ore daily report (07022025).pdf"
Dates live inside the filenames (mtime is just the copy date and is useless for
sorting), so we parse them out. Two formats are present in the wild:
DDMMYYYY -> "(07022025)" = 2025-02-07
YYYYMMDD -> "(20240523)" = 2024-05-23
"""
import os
import re
import time
from datetime import datetime
REPORTS_DIR = os.environ.get(
"REPORTS_DIR", r"C:\Users\DATIS STAR\Desktop\asianmetal_reports"
)
KNOWN_SOURCES = ["Fastmarkets", "FerroAlloyNet", "Delphica", "Platts", "Baiinfo"]
_CACHE: dict = {"ts": 0.0, "index": None}
_TTL = 600 # re-scan disk at most every 10 minutes
_DATE_RE = re.compile(r"(\d{8})")
def _split_source(folder: str):
"""'FerroAlloyNet - Iron Ore' -> ('FerroAlloyNet', 'Iron Ore'); 'Baiinfo China' -> ('Baiinfo', 'China')."""
if " - " in folder:
s, c = folder.split(" - ", 1)
return s.strip(), c.strip()
for s in KNOWN_SOURCES:
if folder.startswith(s):
rest = folder[len(s):].strip()
return s, (rest or s)
parts = folder.split(" ", 1)
return parts[0], (parts[1] if len(parts) > 1 else parts[0])
def _parse_date(name: str):
"""Best-effort report date from a filename. Returns 'YYYY-MM-DD' or None."""
m = _DATE_RE.search(name)
if not m:
return None
s = m.group(1)
candidates = [
(int(s[0:4]), int(s[4:6]), int(s[6:8])), # YYYYMMDD
(int(s[4:8]), int(s[2:4]), int(s[0:2])), # DDMMYYYY
(int(s[4:8]), int(s[0:2]), int(s[2:4])), # MMDDYYYY
]
for y, mo, d in candidates:
if 2000 <= y <= 2099 and 1 <= mo <= 12 and 1 <= d <= 31:
try:
return datetime(y, mo, d).strftime("%Y-%m-%d")
except ValueError:
continue
return None
def _kind(name: str) -> str:
n = name.lower()
if "annual" in n:
return "annual"
if "weekly" in n:
return "weekly"
if "monthly" in n:
return "monthly"
if "daily" in n:
return "daily"
return "report"
def _file_record(entry, source: str, category: str, folder: str) -> dict:
try:
size = entry.stat().st_size
except OSError:
size = 0
return {
"name": entry.name,
"date": _parse_date(entry.name),
"size": size,
"kind": _kind(entry.name),
"source": source,
"category": category,
"folder": folder,
}
def _scan() -> dict:
if not os.path.isdir(REPORTS_DIR):
return {"sources": [], "latest": [], "total": 0, "dir": REPORTS_DIR, "exists": False}
sources: dict = {}
flat: list = []
for entry in os.scandir(REPORTS_DIR):
if not entry.is_dir():
continue
source, category = _split_source(entry.name)
files = []
try:
for f in os.scandir(entry.path):
if f.is_file() and f.name.lower().endswith(".pdf"):
rec = _file_record(f, source, category, entry.name)
files.append(rec)
flat.append(rec)
except OSError:
continue
files.sort(key=lambda r: (r["date"] or "0000-00-00"), reverse=True)
sources.setdefault(source, {})[category] = {
"folder": entry.name,
"category": category,
"count": len(files),
"latest": files[0]["date"] if files else None,
"preview": files[:3],
}
src_list = []
for source, cats in sources.items():
cat_list = sorted(cats.values(), key=lambda c: c["category"])
dates = [c["latest"] for c in cat_list if c["latest"]]
src_list.append({
"source": source,
"categories": cat_list,
"count": sum(c["count"] for c in cat_list),
"category_count": len(cat_list),
"latest": max(dates) if dates else None,
})
src_list.sort(key=lambda s: s["count"], reverse=True)
flat.sort(key=lambda r: (r["date"] or "0000-00-00"), reverse=True)
return {
"sources": src_list,
"latest": flat[:30],
"total": len(flat),
"dir": REPORTS_DIR,
"exists": True,
}
def get_index(force: bool = False) -> dict:
now = time.time()
if force or _CACHE["index"] is None or now - _CACHE["ts"] > _TTL:
_CACHE["index"] = _scan()
_CACHE["ts"] = now
return _CACHE["index"]
def list_files(folder: str) -> list:
"""Full PDF listing for a single category folder (newest first)."""
base = os.path.realpath(REPORTS_DIR)
path = os.path.realpath(os.path.join(REPORTS_DIR, folder))
if path != base and not path.startswith(base + os.sep):
return []
if not os.path.isdir(path):
return []
source, category = _split_source(folder)
out = []
for f in os.scandir(path):
if f.is_file() and f.name.lower().endswith(".pdf"):
out.append(_file_record(f, source, category, folder))
out.sort(key=lambda r: (r["date"] or "0000-00-00"), reverse=True)
return out
def file_path(folder: str, name: str):
"""Resolve a safe absolute path to a report PDF, or None if out of bounds/missing."""
base = os.path.realpath(REPORTS_DIR)
p = os.path.realpath(os.path.join(REPORTS_DIR, folder, name))
if not p.startswith(base + os.sep):
return None
if not os.path.isfile(p) or not p.lower().endswith(".pdf"):
return None
return p

7
backend/requirements.txt Normal file
View File

@ -0,0 +1,7 @@
fastapi
uvicorn[standard]
pydantic
playwright==1.46.0
requests
yfinance
pandas

76
backend/scrape_te.py Normal file
View File

@ -0,0 +1,76 @@
"""
Scrapes commodity prices from tradingeconomics.com/commodities
using a headless browser. Saves to the SQLite DB.
Run every 10 minutes via cron / Task Scheduler.
"""
from datetime import datetime, timezone
from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout
from db import get_conn, init_db
URL = "https://tradingeconomics.com/commodities"
def scrape() -> list[dict]:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
)
page.goto(URL, wait_until="domcontentloaded", timeout=30000)
try:
page.wait_for_selector("table", timeout=15000)
except PWTimeout:
browser.close()
return []
rows = page.evaluate("""() => {
const results = [];
document.querySelectorAll("table tr").forEach(tr => {
const cells = Array.from(tr.querySelectorAll("td"));
if (cells.length < 4) return;
const name = cells[0]?.innerText?.trim();
const price = cells[1]?.innerText?.trim();
const day = cells[2]?.innerText?.trim();
const pct = cells[3]?.innerText?.trim();
const unit = cells[4]?.innerText?.trim() || "";
if (name && price && !isNaN(parseFloat(price.replace(/,/g,"")))) {
results.push({ name, price, day, pct, unit });
}
});
return results;
}""")
browser.close()
return rows
def save(rows: list[dict]):
if not rows:
print("No data scraped")
return
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
with get_conn() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS te_prices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL,
day_change TEXT,
pct_change TEXT,
unit TEXT,
fetched_at TEXT NOT NULL
)
""")
conn.execute("CREATE INDEX IF NOT EXISTS idx_te_name ON te_prices(name)")
conn.executemany(
"INSERT INTO te_prices(name, price, day_change, pct_change, unit, fetched_at) VALUES(?,?,?,?,?,?)",
[(r["name"], float(r["price"].replace(",","")), r["day"], r["pct"], r["unit"], now)
for r in rows]
)
print(f"Saved {len(rows)} prices at {now}")
if __name__ == "__main__":
init_db()
rows = scrape()
print(f"Scraped {len(rows)} rows")
for r in rows[:8]:
print(r)
save(rows)

72
backend/scrape_tgju.py Normal file
View File

@ -0,0 +1,72 @@
"""
Scrapes live Iranian market prices from tgju.org.
Tables: gold, coins, currency, melted gold, silver, oil
"""
from datetime import datetime, timezone
from playwright.sync_api import sync_playwright
from db import get_conn, init_db
URL = "https://www.tgju.org/gold-chart"
def create_table():
with get_conn() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS tgju_prices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, price TEXT, change_val TEXT,
pct_change TEXT, low TEXT, high TEXT,
fetched_at TEXT NOT NULL
)
""")
conn.execute("CREATE INDEX IF NOT EXISTS idx_tgju_name ON tgju_prices(name)")
def scrape() -> list[dict]:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
)
page.goto(URL, wait_until="networkidle", timeout=30000)
rows = page.evaluate("""() => {
const results = [];
document.querySelectorAll("table.data-table.market-table").forEach(table => {
Array.from(table.querySelectorAll("tbody tr")).forEach(tr => {
const cells = Array.from(tr.querySelectorAll("td"));
if (cells.length < 2) return;
const name = cells[0]?.innerText?.trim();
const price = cells[1]?.innerText?.trim();
const change = cells[2]?.innerText?.trim() || "";
const low = cells[3]?.innerText?.trim() || "";
const high = cells[4]?.innerText?.trim() || "";
if (name && price && price !== "-") {
results.push({ name, price, change, low, high });
}
});
});
return results;
}""")
browser.close()
return rows
def save(rows: list[dict]):
if not rows:
print("No data")
return
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
with get_conn() as conn:
conn.executemany(
"INSERT INTO tgju_prices(name,price,change_val,pct_change,low,high,fetched_at) VALUES(?,?,?,?,?,?,?)",
[(r["name"], r["price"], r["change"], "", r["low"], r["high"], now) for r in rows]
)
print(f"Saved {len(rows)} rows at {now}")
for r in rows[:8]:
print(f" {r['name']}: {r['price']} ({r['change']})")
if __name__ == "__main__":
init_db()
create_table()
rows = scrape()
print(f"Scraped {len(rows)} rows")
save(rows)

View File

@ -0,0 +1,55 @@
"""Focused regression checks for authentication and proxy hardening."""
import os
import tempfile
tmp_dir = tempfile.mkdtemp(prefix="steelstatista-security-")
os.environ["DB_PATH"] = os.path.join(tmp_dir, "security-test.db")
os.environ["REPORTS_DIR"] = os.path.join(tmp_dir, "reports")
os.environ["SEED_ADMIN"] = "0"
os.environ["COOKIE_SECURE"] = "0"
os.environ["ALLOWED_ORIGINS"] = "http://localhost:5174,https://web.didvan.com"
os.environ["API_TOKEN"] = "didvan-regression-token"
from fastapi.testclient import TestClient
import main
from db import get_conn
client = TestClient(main.app)
for path in (
"/api/latest",
"/api/currency",
"/api/reports",
"/api/reports/file?folder=x&name=x.pdf",
):
response = client.get(path)
assert response.status_code == 401, (path, response.status_code, response.text)
legacy = client.get("/api/sso?t=credential-must-not-be-read", follow_redirects=False)
assert legacy.status_code == 303, legacy.text
assert legacy.headers["location"] == "/login"
partner = client.get(
"/api/v1/prices/latest",
headers={"Authorization": "Bearer didvan-regression-token"},
)
assert partner.status_code == 200, partner.text
partner_preflight = client.options(
"/api/v1/prices/latest",
headers={
"Origin": "https://web.didvan.com",
"Access-Control-Request-Method": "GET",
"Access-Control-Request-Headers": "authorization",
},
)
assert partner_preflight.status_code == 200, partner_preflight.text
assert partner_preflight.headers["access-control-allow-origin"] == "https://web.didvan.com"
assert main._resolved_client_ip("203.0.113.8", "1.2.3.4", None) == "203.0.113.8"
assert main._resolved_client_ip("127.0.0.1", "1.2.3.4", None) == "1.2.3.4"
with get_conn() as conn:
assert not conn.execute("SELECT 1 FROM users WHERE lower(username)='admin'").fetchone()
print("security smoke tests passed")

182
backend/steel_stocks.py Normal file
View File

@ -0,0 +1,182 @@
"""
Scrapes the largest steel-industry companies by market cap from
companiesmarketcap.com every 5 minutes. Keeps a current snapshot in memory
(served by /api/steel-stocks) and upserts one row per company in steel_stocks.
"""
import asyncio
import os
from datetime import datetime, timezone
from playwright.sync_api import sync_playwright
from db import get_conn
# companiesmarketcap.com is geo-blocked from Iran; route the headless browser
# through an outbound proxy when one is configured (SCRAPER_PROXY / HTTPS_PROXY).
def _proxy_arg() -> dict:
server = (
os.environ.get("SCRAPER_PROXY")
or os.environ.get("HTTPS_PROXY")
or os.environ.get("HTTP_PROXY")
)
return {"proxy": {"server": server}} if server else {}
URL = "https://companiesmarketcap.com/steel-industry/largest-companies-by-market-cap/"
UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
)
INTERVAL_SECONDS = 5 * 60
_latest: list[dict] = []
def create_table() -> None:
with get_conn() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS steel_stocks (
code TEXT PRIMARY KEY,
rank INTEGER,
name TEXT,
market_cap REAL,
price REAL,
today_pct REAL,
dir TEXT,
country TEXT,
fetched_at TEXT
)
""")
def _money(s):
"""'$254.39' -> 254.39 ; '$2,000' -> 2000.0"""
try:
return float(str(s).replace("$", "").replace(",", "").strip())
except (ValueError, AttributeError):
return None
def _market_cap_b(s):
"""'$57.93 B' -> 57.93 ; '$850.00 M' -> 0.85 ; '$1.20 T' -> 1200.0 (billions)"""
t = str(s).replace("$", "").replace(",", "").strip().upper()
mult = 1.0
if t.endswith("T"):
t, mult = t[:-1], 1000.0
elif t.endswith("B"):
t, mult = t[:-1], 1.0
elif t.endswith("M"):
t, mult = t[:-1], 0.001
try:
return round(float(t.strip()) * mult, 4)
except ValueError:
return None
def _scrape_sync() -> list[dict]:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, **_proxy_arg())
page = browser.new_page(user_agent=UA)
try:
page.goto(URL, wait_until="domcontentloaded", timeout=45000)
page.wait_for_selector("table tbody tr", timeout=20000)
# Prices load via JS after the table renders — wait so they're populated.
page.wait_for_timeout(5000)
raw = page.evaluate(r"""() => {
const out = [];
document.querySelectorAll("table tbody tr").forEach(tr => {
const tds = tr.querySelectorAll("td");
if (tds.length < 8) return;
const name = (tr.querySelector(".company-name")?.innerText || "").trim();
const code = (tr.querySelector(".company-code")?.innerText || "").trim();
if (!name) return;
const chg = tds[5];
const blob = ((chg?.innerHTML || "") + " " + (chg?.className || "")).toLowerCase();
let dir = "";
if (blob.includes("green")) dir = "up";
else if (blob.includes("red")) dir = "down";
out.push({
rank: (tds[1]?.innerText || "").trim(),
name, code,
market_cap: (tds[3]?.innerText || "").trim(),
price: (tds[4]?.innerText || "").trim(),
today: (chg?.innerText || "").trim(),
dir,
country: (tds[7]?.innerText || "").trim(),
});
});
return out;
}""")
finally:
browser.close()
rows = []
for r in raw:
try:
rank = int(r["rank"])
except (ValueError, TypeError):
rank = None
pct = _money(str(r["today"]).replace("%", ""))
if pct is not None and r["dir"] == "down":
pct = -pct
rows.append({
"code": r["code"],
"rank": rank,
"name": r["name"],
"market_cap": _market_cap_b(r["market_cap"]),
"price": _money(r["price"]),
"today_pct": pct,
"dir": r["dir"],
"country": r["country"],
})
return rows
def _save(rows: list[dict], now: str) -> None:
with get_conn() as conn:
for r in rows:
if not r["code"]:
continue
conn.execute(
"""INSERT INTO steel_stocks
(code, rank, name, market_cap, price, today_pct, dir, country, fetched_at)
VALUES(?,?,?,?,?,?,?,?,?)
ON CONFLICT(code) DO UPDATE SET
rank=excluded.rank, name=excluded.name, market_cap=excluded.market_cap,
price=excluded.price, today_pct=excluded.today_pct, dir=excluded.dir,
country=excluded.country, fetched_at=excluded.fetched_at""",
(r["code"], r["rank"], r["name"], r["market_cap"], r["price"],
r["today_pct"], r["dir"], r["country"], now),
)
def _load_from_db() -> None:
global _latest
try:
with get_conn() as conn:
rows = conn.execute(
"SELECT * FROM steel_stocks ORDER BY rank IS NULL, rank"
).fetchall()
if rows:
_latest = [dict(r) for r in rows]
except Exception as e:
print(f"[steel] could not seed from DB: {e}")
async def scrape_loop() -> None:
global _latest
create_table()
_load_from_db()
while True:
try:
rows = await asyncio.to_thread(_scrape_sync)
if rows:
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
await asyncio.to_thread(_save, rows, now)
rows.sort(key=lambda r: (r["rank"] is None, r["rank"] or 9999))
_latest = [{**r, "fetched_at": now} for r in rows]
print(f"[steel] {len(rows)} companies at {now}")
except Exception as e:
print(f"[steel] error: {e}")
await asyncio.sleep(INTERVAL_SECONDS)
def get_latest() -> list[dict]:
return _latest

136
backend/sync.py Normal file
View File

@ -0,0 +1,136 @@
"""
Daily sync: fetches latest prices from SteelStatista.com API and inserts
only NEW (title, date) pairs into the SQLite database.
Run manually or via Windows Task Scheduler once a day.
"""
import os
import requests
import time
from db import get_conn, init_db
# The data-source token is NEVER hardcoded. Set it via the admin panel
# (stored in app_config) or the ASIANMETAL_TOKEN environment variable.
HEADERS = {
"lang": "fa", "platform": "web", "accept": "application/json",
"referer": "https://SteelStatista.com/",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
}
BASE = "https://api.SteelStatista.com"
CAT_IDS = [9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,
31,32,33,34,35,36,37,38,39,40,42,43,44,45,46,47,48,49,50,51,52,53,
54,55,56,57,65,67,68,70,71,72,74,75,76,77,78,79,80,81,82,83,84,85,
87,88,89,90,92,93,94,95,96,97,98,99,100,101,102,103,104,105]
CAT_NAMES = {1:"Base Metals",2:"Minor Metals",3:"Ferroalloys",4:"Rare Earths",
5:"Carbon Steel",6:"Stainless & Special",7:"Steel Raw Materials",8:"Refractories",
9:"Aluminum",10:"Copper",11:"Lead",12:"Nickel",13:"Tin",14:"Zinc",
15:"Antimony",16:"Arsenic",17:"Bismuth",18:"Cadmium",19:"Calcium",
20:"Chromium",21:"Cobalt",22:"Gallium",23:"Germanium",24:"Indium",
25:"Lithium",26:"Magnesium",27:"Manganese",28:"Mercury",29:"Molybdenum",
30:"Niobium",31:"Rhenium",32:"Selenium",33:"Silicon",34:"Strontium",
35:"Tantalum",36:"Tellurium",37:"Titanium",38:"Tungsten",39:"Vanadium",
40:"Zirconium",42:"Ferroboron",43:"Ferrochrome",44:"Ferromanganese",
45:"Ferromolybdenum",46:"Ferronickel",47:"Ferroniobium",48:"Ferrophosphorus",
49:"Ferrosilicon",50:"Ferrotitanium",51:"Ferrotungsten",52:"Ferrovanadium",
53:"Silicomanganese",54:"Chromium Silicon",55:"Calcium Silicon",
56:"Chrome Ore",57:"Manganese Ore",65:"Lutetium",67:"Neodymium",
68:"Praseodymium",70:"Samarium",71:"Scandium",72:"Terbium",
74:"Ytterbium",75:"Yttrium",76:"Wire Rod",77:"Rebar",78:"Sections",
79:"Pipe",80:"Hot Rolled Coil",81:"Cold Rolled Coil",82:"Plate",
83:"Coated",84:"Strip",85:"Stainless Bar",87:"Stainless Pipe",
88:"Stainless Scrap",89:"Bearing Steel",90:"Cold Heading Steel",
92:"Silicon Steel",93:"Structural Steel",94:"Coal",95:"Coke",
96:"Iron",97:"Iron Ore",98:"Steel Billet",99:"Steel Scrap",
100:"Calcined Bauxite",101:"Carbon",102:"Fused Alumina",
103:"Graphite",104:"Magnesia",105:"Silicon Carbide"}
PARENT_MAP = {9:1,10:1,11:1,12:1,13:1,14:1,15:2,16:2,17:2,18:2,19:2,20:2,
21:2,22:2,23:2,24:2,25:2,26:2,27:2,28:2,29:2,30:2,31:2,32:2,
33:2,34:2,35:2,36:2,37:2,38:2,39:2,40:2,42:3,43:3,44:3,45:3,
46:3,47:3,48:3,49:3,50:3,51:3,52:3,53:3,54:3,55:3,56:3,57:3,
65:4,67:4,68:4,70:4,71:4,72:4,74:4,75:4,76:5,77:5,78:5,79:5,
80:5,81:5,82:5,83:5,84:5,85:6,87:6,88:6,89:6,90:6,92:6,93:6,
94:7,95:7,96:7,97:7,98:7,99:7,100:8,101:8,102:8,103:8,104:8,105:8}
def get_latest_dates(conn) -> dict:
rows = conn.execute("SELECT title, MAX(date) as d FROM prices GROUP BY title").fetchall()
return {r["title"]: r["d"] for r in rows}
def get_token() -> str:
"""Read the SteelStatista token from app_config (editable via the admin panel);
fall back to the bundled default if not set."""
try:
with get_conn() as conn:
row = conn.execute(
"SELECT value FROM app_config WHERE key='asianmetal_token'"
).fetchone()
if row and row["value"]:
return row["value"]
except Exception:
pass
return os.environ.get("ASIANMETAL_TOKEN", "")
def sync():
init_db()
session = requests.Session()
session.headers.update(HEADERS)
session.headers["Authorization"] = f"Bearer {get_token()}"
with get_conn() as conn:
latest_dates = get_latest_dates(conn)
print("Collecting symbols...")
symbols = []
for cat_id in CAT_IDS:
try:
r = session.get(f"{BASE}/Symbol?categoryId={cat_id}", timeout=10)
for sym in r.json():
symbols.append({
"symbol_id": sym["id"],
"grp": CAT_NAMES.get(PARENT_MAP.get(cat_id, 0), ""),
"category": CAT_NAMES.get(cat_id, str(cat_id)),
"title": sym["title"],
})
except Exception as e:
print(f" Error cat {cat_id}: {e}")
time.sleep(0.15)
print(f"Found {len(symbols)} symbols. Fetching new prices...")
inserted = 0
for i, sym in enumerate(symbols):
title = sym["title"]
cutoff = latest_dates.get(title)
try:
r = session.get(f"{BASE}/Symbol/{sym['symbol_id']}", timeout=10)
prices = r.json().get("symbolPrices", [])
rows_to_insert = [
(sym["grp"], sym["category"], title, p["date"][:10], p["low"], p["mid"], p["high"])
for p in prices
if not (cutoff and p["date"][:10] <= cutoff)
]
if rows_to_insert:
with get_conn() as conn:
conn.executemany(
"INSERT OR IGNORE INTO prices(grp,category,title,date,low,mid,high) VALUES(?,?,?,?,?,?,?)",
rows_to_insert
)
inserted += len(rows_to_insert)
if (i + 1) % 100 == 0:
print(f" {i+1}/{len(symbols)} done — {inserted} new rows so far")
except Exception as e:
print(f" Error symbol {sym['symbol_id']}: {e}")
time.sleep(0.2)
print(f"\nSync complete — {inserted} new rows inserted")
if __name__ == "__main__":
sync()

162
backend/world_economy.py Normal file
View File

@ -0,0 +1,162 @@
"""
Scrapes the world-economy indicators matrix from Trading Economics
(tradingeconomics.com/matrix) the same data behind TradingView's
"world-economy indicators heatmap". Macro data changes slowly, so this
refreshes every few hours. Caches the leading economies in memory
(served by /api/world-economy) and persists all countries.
"""
import asyncio
import os
from datetime import datetime, timezone
from playwright.sync_api import sync_playwright
from db import get_conn
# Trading Economics is geo-blocked from Iran; route the headless browser through
# an outbound proxy when one is configured (SCRAPER_PROXY / HTTPS_PROXY).
def _proxy_arg() -> dict:
server = (
os.environ.get("SCRAPER_PROXY")
or os.environ.get("HTTPS_PROXY")
or os.environ.get("HTTP_PROXY")
)
return {"proxy": {"server": server}} if server else {}
URL = "https://tradingeconomics.com/matrix"
UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
)
INTERVAL_SECONDS = 7 * 24 * 3600 # 1 week (macro data changes slowly)
# Leading economies to surface (must match Trading Economics' country names).
COUNTRIES = [
"United States", "China", "Euro Area", "Germany", "Japan", "India",
"United Kingdom", "France", "Italy", "Canada", "South Korea", "Russia",
"Brazil", "Australia", "Spain", "Mexico", "Turkey", "Indonesia",
]
_latest: list[dict] = []
def create_table() -> None:
with get_conn() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS world_economy (
country TEXT PRIMARY KEY,
gdp REAL,
gdp_growth REAL,
interest_rate REAL,
inflation_rate REAL,
jobless_rate REAL,
gov_budget REAL,
debt_gdp REAL,
current_account REAL,
population REAL,
fetched_at TEXT
)
""")
def _num(s):
try:
return float(str(s).replace(",", "").replace("%", "").strip())
except (ValueError, AttributeError):
return None
def _scrape_sync() -> list[dict]:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, **_proxy_arg())
page = browser.new_page(user_agent=UA)
try:
page.goto(URL, wait_until="domcontentloaded", timeout=45000)
page.wait_for_selector("table tbody tr", timeout=20000)
rows = page.evaluate(r"""() => {
const t = document.querySelector("table");
if (!t) return [];
return Array.from(t.querySelectorAll("tbody tr")).map(tr =>
Array.from(tr.querySelectorAll("th,td")).map(c => (c.innerText || "").trim()));
}""")
finally:
browser.close()
out = []
for r in rows:
if len(r) < 10 or not r[0]:
continue
out.append({
"country": r[0],
"gdp": _num(r[1]),
"gdp_growth": _num(r[2]),
"interest_rate": _num(r[3]),
"inflation_rate": _num(r[4]),
"jobless_rate": _num(r[5]),
"gov_budget": _num(r[6]),
"debt_gdp": _num(r[7]),
"current_account": _num(r[8]),
"population": _num(r[9]),
})
return out
def _save(rows: list[dict], now: str) -> None:
with get_conn() as conn:
for r in rows:
conn.execute(
"""INSERT INTO world_economy
(country, gdp, gdp_growth, interest_rate, inflation_rate, jobless_rate,
gov_budget, debt_gdp, current_account, population, fetched_at)
VALUES(?,?,?,?,?,?,?,?,?,?,?)
ON CONFLICT(country) DO UPDATE SET
gdp=excluded.gdp, gdp_growth=excluded.gdp_growth,
interest_rate=excluded.interest_rate, inflation_rate=excluded.inflation_rate,
jobless_rate=excluded.jobless_rate, gov_budget=excluded.gov_budget,
debt_gdp=excluded.debt_gdp, current_account=excluded.current_account,
population=excluded.population, fetched_at=excluded.fetched_at""",
(r["country"], r["gdp"], r["gdp_growth"], r["interest_rate"], r["inflation_rate"],
r["jobless_rate"], r["gov_budget"], r["debt_gdp"], r["current_account"],
r["population"], now),
)
def _leading(rows: list[dict], now: str) -> list[dict]:
order = {c: i for i, c in enumerate(COUNTRIES)}
kept = [r for r in rows if r["country"] in order]
kept.sort(key=lambda r: order[r["country"]])
return [{**r, "fetched_at": now} for r in kept]
def _load_from_db() -> None:
"""Seed the cache from the DB on startup so the API has data before the
first scrape completes."""
global _latest
try:
with get_conn() as conn:
rows = conn.execute("SELECT * FROM world_economy").fetchall()
if rows:
data = [dict(r) for r in rows]
now = data[0].get("fetched_at") or ""
_latest = _leading(data, now) or _latest
except Exception as e:
print(f"[world] could not seed from DB: {e}")
async def scrape_loop() -> None:
global _latest
create_table()
_load_from_db()
while True:
try:
rows = await asyncio.to_thread(_scrape_sync)
if rows:
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
await asyncio.to_thread(_save, rows, now)
_latest = _leading(rows, now)
print(f"[world] {len(_latest)}/{len(rows)} leading economies at {now}")
except Exception as e:
print(f"[world] error: {e}")
await asyncio.sleep(INTERVAL_SECONDS)
def get_latest() -> list[dict]:
return _latest

23
deploy/.env.example Normal file
View File

@ -0,0 +1,23 @@
# Copy to deploy/.env and fill in real values — NEVER commit the real .env.
# Consumed by deploy/docker-compose.yml on the self-hosted VPS.
# Admin-panel (admin_app) password — required only if you run the admin service.
ADMIN_PASSWORD=change-me-strong-secret
# Public origin of the deployed site (used for the CORS allow-list).
# e.g. http://94.101.187.12 or https://yourdomain.ir
PUBLIC_ORIGIN=http://your-server-ip
# data-source token for the daily metals/steel sync (rotates ~every 4 days).
# Leave blank until you have a current one.
ASIANMETAL_TOKEN=
# SSO shared secret. MUST be byte-identical to the steelforesight panel's
# SSO_SECRET — anyone holding it can mint a login token for any email. If unset,
# SSO degrades to the normal login page (no crash).
SSO_SECRET=
# Bearer token for the internal /api/v1 data API (parent org). Anyone holding it
# can read all price data. If unset, /api/v1 returns 503. Generate with e.g.
# `python -c "import secrets; print(secrets.token_urlsafe(24))"`.
API_TOKEN=

14
deploy/deploy.sh Normal file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Run this ON the Arvan VPS (inside the repo) to ship the latest code.
# ssh <user>@94.101.187.12 'cd /path/to/asianmetal-dashboard && bash deploy/deploy.sh'
set -euo pipefail
cd "$(dirname "$0")/.."
echo "▶ pulling latest…"
git pull --ff-only
echo "▶ building + (re)starting containers…"
docker compose -f deploy/docker-compose.yml up -d --build
echo "▶ status:"
docker compose -f deploy/docker-compose.yml ps

47
deploy/docker-compose.yml Normal file
View File

@ -0,0 +1,47 @@
services:
backend:
container_name: statista-backend-1
build:
context: ../backend
dockerfile: Dockerfile
restart: always
environment:
- DB_PATH=/data/asianmetal.db
- REPORTS_DIR=/data/reports
- API_TOKEN=b9660e2c174a5c76b958afdef8cc7d95f71abe23155299832b94984fcadf9f73
- API_TOKENS=client:b9660e2c174a5c76b958afdef8cc7d95f71abe23155299832b94984fcadf9f73,didvan:didvan_statista_key_2026,statista_secret_token_2026:b9660e2c174a5c76b958afdef8cc7d95f71abe23155299832b94984fcadf9f73
- SSO_SECRET=wSoNQxqlABZylZpXfuvUauUtaWXlNiJUxiOB9lBXOj8
- ALLOWED_ORIGINS=https://statista.steelforesight.ir,http://statista.steelforesight.ir,https://steelforesight.ir,http://steelforesight.ir,https://app.didvan.com,http://app.didvan.com,https://iranianfuturist.com,http://iranianfuturist.com,http://94.101.187.12:7860,http://localhost:3000,http://localhost:5173,http://localhost:5174,http://127.0.0.1:5174
- TRUSTED_PROXIES=127.0.0.1/32,172.16.0.0/12,10.0.0.0/8,192.168.0.0/16
volumes:
- statista_data:/data
ports:
- "127.0.0.1:8000:8000"
frontend:
container_name: statista-frontend-1
build:
context: ../frontend
dockerfile: Dockerfile
restart: always
ports:
- "127.0.0.1:3000:3000"
nginx:
container_name: statista-nginx-1
image: nginx:alpine
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./certs:/etc/nginx/certs:ro
- ./certs-git:/etc/nginx/certs-git:ro
- ./certbot-www:/var/www/certbot:ro
depends_on:
- backend
- frontend
volumes:
statista_data:

415
deploy/nginx.conf Normal file
View File

@ -0,0 +1,415 @@
resolver 127.0.0.11 valid=300s ipv6=off;
resolver_timeout 5s;
# 1. Gitea (git.irfartak.com) - HTTP
server {
listen 80;
server_name git.irfartak.com;
client_max_body_size 512M;
location / {
set $upstream_gitea http://gitea-srv:3000;
proxy_pass $upstream_gitea;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# 2. Gitea (git.irfartak.com) - HTTPS
server {
listen 443 ssl;
server_name git.irfartak.com;
ssl_certificate /etc/nginx/certs-git/fullchain.pem;
ssl_certificate_key /etc/nginx/certs-git/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
client_max_body_size 512M;
location / {
set $upstream_gitea http://gitea-srv:3000;
proxy_pass $upstream_gitea;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
}
# 3. Statista (statista.steelforesight.ir) - HTTP
server {
listen 80;
server_name statista.steelforesight.ir;
location /api/ {
set $upstream_backend http://statista-backend-1:8000;
proxy_pass $upstream_backend;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ws/ {
set $upstream_backend http://statista-backend-1:8000;
proxy_pass $upstream_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Smart Routing for root /:
# If request has Authorization header OR query param token/api_token, serve JSON data (/api/tree).
# Otherwise, if it is a normal browser visit, serve React Dashboard UI.
location = / {
set $backend_target "0";
if ($http_authorization ~* ".+") {
set $backend_target "1";
}
if ($arg_api_token ~* ".+") {
set $backend_target "1";
}
if ($arg_token ~* ".+") {
set $backend_target "1";
}
if ($http_accept ~* "application/json") {
set $backend_target "1";
}
if ($backend_target = "1") {
rewrite ^ /api/tree break;
proxy_pass http://statista-backend-1:8000;
}
set $upstream_frontend http://if-react-frontend:80;
proxy_pass $upstream_frontend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
set $upstream_frontend http://if-react-frontend:80;
proxy_pass $upstream_frontend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# 4. Statista (statista.steelforesight.ir) - HTTPS (Port 443)
server {
listen 443 ssl;
server_name statista.steelforesight.ir;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location /api/ {
set $upstream_backend http://statista-backend-1:8000;
proxy_pass $upstream_backend;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
location /ws/ {
set $upstream_backend http://statista-backend-1:8000;
proxy_pass $upstream_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
# Smart Routing for root /:
# If request has Authorization header OR query param token/api_token, serve JSON data (/api/tree).
# Otherwise, if it is a normal browser visit, serve React Dashboard UI.
location = / {
set $backend_target "0";
if ($http_authorization ~* ".+") {
set $backend_target "1";
}
if ($arg_api_token ~* ".+") {
set $backend_target "1";
}
if ($arg_token ~* ".+") {
set $backend_target "1";
}
if ($http_accept ~* "application/json") {
set $backend_target "1";
}
if ($backend_target = "1") {
rewrite ^ /api/tree break;
proxy_pass http://statista-backend-1:8000;
}
set $upstream_frontend http://if-react-frontend:80;
proxy_pass $upstream_frontend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
location / {
set $upstream_frontend http://if-react-frontend:80;
proxy_pass $upstream_frontend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
}
# 5. Iranian Futurist Main Portal
server {
listen 80;
server_name iranianfuturist.com www.iranianfuturist.com;
location / {
set $upstream_if http://if-react-frontend:80;
proxy_pass $upstream_if;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# 6. IRFartak
server {
listen 80;
server_name irfartak.com www.irfartak.com;
location /api/ {
set $upstream_fartak_api http://irfartak-backend-1:3000;
proxy_pass $upstream_fartak_api;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
set $upstream_fartak_fe http://irfartak-frontend-1:3000;
proxy_pass $upstream_fartak_fe;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# 7. Default Fallback Server
server {
listen 80 default_server;
server_name _;
location /statista-api/ {
rewrite ^/statista-api/(.*)$ /api/$1 break;
set $upstream_backend http://statista-backend-1:8000;
proxy_pass $upstream_backend;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
set $upstream_if http://if-react-frontend:80;
proxy_pass $upstream_if;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Iranian Futurist - HTTPS
server {
listen 443 ssl;
server_name iranianfuturist.com www.iranianfuturist.com;
ssl_certificate /etc/nginx/certs-git/fullchain.pem;
ssl_certificate_key /etc/nginx/certs-git/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
set $upstream_if http://if-react-frontend:80;
proxy_pass $upstream_if;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
}
# 6b. IRFartak .com HTTPS
server {
listen 443 ssl;
server_name irfartak.com www.irfartak.com;
ssl_certificate /etc/nginx/certs-git/fullchain.pem;
ssl_certificate_key /etc/nginx/certs-git/privkey.pem;
location / {
set $up_a http://irfartak-frontend-1:3000;
proxy_pass $up_a;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection upgrade;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# 6c. IRFartak .ir HTTP
server {
listen 80;
server_name irfartak.ir www.irfartak.ir;
location / {
set $up_b http://irfartak-frontend-1:3000;
proxy_pass $up_b;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection upgrade;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# 6d. IRFartak .ir HTTPS
server {
listen 443 ssl;
server_name irfartak.ir www.irfartak.ir;
ssl_certificate /etc/nginx/certs-git/fullchain.pem;
ssl_certificate_key /etc/nginx/certs-git/privkey.pem;
location / {
set $up_c http://irfartak-frontend-1:3000;
proxy_pass $up_c;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection upgrade;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# 9. TLS catch-all - unknown SNI must not land on Gitea
server {
listen 443 ssl default_server;
server_name _;
ssl_certificate /etc/nginx/certs-git/fullchain.pem;
ssl_certificate_key /etc/nginx/certs-git/privkey.pem;
return 444;
}
# --- CMS (Directus) ---
server {
listen 80;
server_name cms.irfartak.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type "text/plain";
}
location / {
resolver 127.0.0.11 valid=30s;
set $upstream_cms http://cms:8055;
proxy_pass $upstream_cms;
proxy_http_version 1.1;
client_max_body_size 100m;
proxy_read_timeout 300s;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# --- CMS (Directus) HTTPS ---
server {
listen 443 ssl;
server_name cms.irfartak.com;
ssl_certificate /etc/nginx/certs/cms/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/cms/privkey.pem;
location / {
resolver 127.0.0.11 valid=30s;
set $upstream_cms_s http://cms:8055;
proxy_pass $upstream_cms_s;
proxy_http_version 1.1;
client_max_body_size 100m;
proxy_read_timeout 300s;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
}

12
deploy/renew-hook.sh Normal file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
# certbot --deploy-hook: snap-confined Docker can't mount /etc/letsencrypt, so
# copy the renewed cert into the project dir (mounted into nginx) and reload.
set -e
DEST=/root/statista/deploy/certs
LIVE=/etc/letsencrypt/live/statista.steelforesight.ir
mkdir -p "$DEST"
cp -L "$LIVE/fullchain.pem" "$DEST/fullchain.pem"
cp -L "$LIVE/privkey.pem" "$DEST/privkey.pem"
chmod 644 "$DEST/fullchain.pem"
chmod 600 "$DEST/privkey.pem"
docker exec deploy-nginx-1 nginx -s reload

40
docker-compose.yml Normal file
View File

@ -0,0 +1,40 @@
# Run the server stack (backend API + admin panel) on any Docker host.
# docker compose up -d --build
# Set secrets in a .env file next to this (ADMIN_PASSWORD, ASIANMETAL_TOKEN, ...).
services:
api:
build:
context: ./backend
image: didvan-api
restart: unless-stopped
ports:
- "8000:8000"
environment:
DB_PATH: /data/asianmetal.db
REPORTS_DIR: /data/reports
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-http://localhost:5174}
COOKIE_SECURE: ${COOKIE_SECURE:-0}
COOKIE_SAMESITE: ${COOKIE_SAMESITE:-lax}
SEED_ADMIN: ${SEED_ADMIN:-0}
TRUSTED_PROXY_CIDRS: ${TRUSTED_PROXY_CIDRS:-127.0.0.1/32,::1/128,172.16.0.0/12}
ADMIN_PASSWORD: ${ADMIN_PASSWORD:?set ADMIN_PASSWORD in .env}
ASIANMETAL_TOKEN: ${ASIANMETAL_TOKEN:-}
volumes:
- didvan-data:/data
# Chromium needs more shared memory than the 64MB default
shm_size: "1gb"
# Admin panel — keep it OFF public networks. Uncomment to run it (local/VPN only).
# admin:
# build: { context: . , dockerfile: admin/Dockerfile }
# image: didvan-admin
# restart: unless-stopped
# ports: ["127.0.0.1:8001:8001"]
# environment:
# DB_PATH: /data/asianmetal.db
# ADMIN_USER: ${ADMIN_USER:-admin}
# ADMIN_PASSWORD: ${ADMIN_PASSWORD:?set ADMIN_PASSWORD in .env}
# volumes: [ "didvan-data:/data" ]
volumes:
didvan-data:

14
frontend/.liaraignore Normal file
View File

@ -0,0 +1,14 @@
node_modules
.output
.vinxi
.nitro
.tanstack
dist
dist-ssr
.env
.env.*
*.db
*.sqlite*
__pycache__
*.pyc
.git

View File

@ -0,0 +1,4 @@
{
"schemaVersion": 1,
"template": "tanstack_start_ts_2026-05-29"
}

8
frontend/.prettierignore Normal file
View File

@ -0,0 +1,8 @@
node_modules
dist
.output
.vinxi
pnpm-lock.yaml
package-lock.json
bun.lock
routeTree.gen.ts

6
frontend/.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"printWidth": 100,
"semi": true,
"singleQuote": false,
"trailingComma": "all"
}

20
frontend/Dockerfile Normal file
View File

@ -0,0 +1,20 @@
# Frontend (TanStack Start SSR) for self-hosted Docker deploy.
# On Liara this folder is a Node buildpack and this Dockerfile is ignored;
# it's only used by the VPS docker-compose.
FROM node:22-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
# "/" -> API_BASE becomes "" (relative), so the browser calls /api on the same
# origin and nginx proxies it to the backend. Avoids the Liara prod fallback URL.
ENV VITE_API_BASE="/"
RUN npm run build
FROM node:22-slim AS run
WORKDIR /app
ENV NODE_ENV=production \
PORT=3000
COPY --from=build /app/.output ./.output
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

1325
frontend/bun.lock Normal file

File diff suppressed because it is too large Load Diff

6
frontend/bunfig.toml Normal file
View File

@ -0,0 +1,6 @@
[install]
# 24h supply-chain guard: skip package versions published less than a day ago.
minimumReleaseAge = 86400
# Each entry bypasses the 24h guard for one package — confirm with the user
# before adding any.
minimumReleaseAgeExcludes = ["@lovable.dev/vite-tanstack-config"]

22
frontend/components.json Normal file
View File

@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"css": "src/styles.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}

40
frontend/eslint.config.js Normal file
View File

@ -0,0 +1,40 @@
import js from "@eslint/js";
import eslintPluginPrettier from "eslint-plugin-prettier/recommended";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["dist", ".output", ".vinxi"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"no-restricted-imports": [
"error",
{
paths: [
{
name: "server-only",
message:
"TanStack Start does not use the Next.js `server-only` package. Rename the module to `*.server.ts` or mark it with `@tanstack/react-start/server-only`.",
},
],
},
],
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"@typescript-eslint/no-unused-vars": "off",
},
},
eslintPluginPrettier,
);

8
frontend/liara.json Normal file
View File

@ -0,0 +1,8 @@
{
"platform": "node",
"app": "statista-didvan",
"port": 3000,
"build": {
"location": "iran"
}
}

8915
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

100
frontend/package.json Normal file
View File

@ -0,0 +1,100 @@
{
"name": "asianmetal-dashboard",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"build:dev": "vite build --mode development",
"preview": "vite preview",
"start": "node .output/server/index.mjs",
"lint": "eslint .",
"format": "prettier --write ."
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.8",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.2.1",
"@tanstack/react-query": "^5.83.0",
"@tanstack/react-router": "^1.168.25",
"@tanstack/react-start": "^1.167.50",
"@tanstack/router-plugin": "^1.167.28",
"@types/papaparse": "^5.5.2",
"@types/three": "^0.184.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"flag-icons": "^7.5.0",
"framer-motion": "^12.40.0",
"gsap": "^3.15.0",
"input-otp": "^1.4.2",
"jalaali-js": "^1.2.8",
"lucide-react": "^0.575.0",
"ogl": "^1.0.11",
"papaparse": "^5.5.3",
"react": "^19.2.0",
"react-day-picker": "^9.14.0",
"react-dom": "^19.2.0",
"react-hook-form": "^7.71.2",
"react-resizable-panels": "^4.6.5",
"recharts": "^2.15.4",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.1",
"three": "^0.167.1",
"tw-animate-css": "^1.3.4",
"vaul": "^1.1.2",
"vite-tsconfig-paths": "^6.0.2",
"zod": "^3.24.2"
},
"devDependencies": {
"@eslint/js": "^9.32.0",
"@lovable.dev/vite-tanstack-config": "^2.1.1",
"@types/node": "^22.16.5",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^5.0.4",
"eslint": "^9.32.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^15.15.0",
"nitro": "3.0.260429-beta",
"prettier": "^3.7.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.56.1",
"vite": "^7.3.1"
},
"overrides": {
"esbuild": "0.28.1"
}
}

12
frontend/public/icon.svg Normal file
View File

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#e9a93a"/>
<stop offset="1" stop-color="#b9781a"/>
</linearGradient>
</defs>
<rect width="512" height="512" rx="112" fill="url(#g)"/>
<path d="M96 300 L188 300 L228 172 L292 360 L332 256 L360 300 L416 300"
fill="none" stroke="#fff" stroke-width="30"
stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 538 B

View File

@ -0,0 +1,17 @@
{
"name": "سامانه جامع آمار و اطلاعات دیدوان",
"short_name": "دیدوان",
"description": "بازار داخلی ارز، کامودیتی زنده و قیمت فلزات و فولاد",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait",
"dir": "rtl",
"lang": "fa",
"background_color": "#f7f4ec",
"theme_color": "#f7f4ec",
"icons": [
{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" },
{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "maskable" }
]
}

31
frontend/public/sw.js Normal file
View File

@ -0,0 +1,31 @@
// Minimal service worker — enables PWA install + an offline fallback for
// navigations. Live market data is never cached (always hits the network).
const SHELL = "yz-shell-v1";
self.addEventListener("install", (e) => {
e.waitUntil(
caches
.open(SHELL)
.then((c) => c.add("/"))
.catch(() => {}),
);
self.skipWaiting();
});
self.addEventListener("activate", (e) => {
e.waitUntil(
caches
.keys()
.then((keys) => Promise.all(keys.filter((k) => k !== SHELL).map((k) => caches.delete(k))))
.then(() => self.clients.claim()),
);
});
self.addEventListener("fetch", (e) => {
const req = e.request;
// Only handle top-level navigations; let everything else (API, assets) pass through.
if (req.mode !== "navigate") return;
e.respondWith(
fetch(req).catch(() => caches.match("/").then((r) => r || Response.error())),
);
});

View File

@ -0,0 +1,36 @@
.card-swap-container {
position: absolute;
bottom: 0;
right: 0;
transform: translate(5%, 20%);
transform-origin: bottom right;
perspective: 900px;
overflow: visible;
}
.card {
position: absolute;
top: 50%;
left: 50%;
border-radius: 12px;
border: 1px solid #fff;
background: #000;
transform-style: preserve-3d;
will-change: transform;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
@media (max-width: 768px) {
.card-swap-container {
transform: scale(0.75) translate(25%, 25%);
}
}
@media (max-width: 480px) {
.card-swap-container {
transform: scale(0.55) translate(25%, 25%);
}
}

View File

@ -0,0 +1,191 @@
import React, {
Children,
cloneElement,
forwardRef,
isValidElement,
useEffect,
useMemo,
useRef,
} from "react";
import gsap from "gsap";
import "./CardSwap.css";
export const Card = forwardRef(({ customClass, ...rest }, ref) => (
<div ref={ref} {...rest} className={`card ${customClass ?? ""} ${rest.className ?? ""}`.trim()} />
));
Card.displayName = "Card";
const makeSlot = (i, distX, distY, total) => ({
x: i * distX,
y: -i * distY,
z: -i * distX * 1.5,
zIndex: total - i,
});
const placeNow = (el, slot, skew) =>
gsap.set(el, {
x: slot.x,
y: slot.y,
z: slot.z,
xPercent: -50,
yPercent: -50,
skewY: skew,
transformOrigin: "center center",
zIndex: slot.zIndex,
force3D: true,
});
const CardSwap = ({
width = 500,
height = 400,
cardDistance = 60,
verticalDistance = 70,
delay = 5000,
pauseOnHover = false,
onCardClick,
skewAmount = 6,
easing = "elastic",
children,
}) => {
const config =
easing === "elastic"
? {
ease: "elastic.out(0.6,0.9)",
durDrop: 2,
durMove: 2,
durReturn: 2,
promoteOverlap: 0.9,
returnDelay: 0.05,
}
: {
ease: "power1.inOut",
durDrop: 0.8,
durMove: 0.8,
durReturn: 0.8,
promoteOverlap: 0.45,
returnDelay: 0.2,
};
const childArr = useMemo(() => Children.toArray(children), [children]);
const refs = useMemo(
() => childArr.map(() => React.createRef()),
// eslint-disable-next-line react-hooks/exhaustive-deps
[childArr.length],
);
const order = useRef(Array.from({ length: childArr.length }, (_, i) => i));
const tlRef = useRef(null);
const intervalRef = useRef();
const container = useRef(null);
useEffect(() => {
const total = refs.length;
refs.forEach((r, i) =>
placeNow(r.current, makeSlot(i, cardDistance, verticalDistance, total), skewAmount),
);
const swap = () => {
if (order.current.length < 2) return;
const [front, ...rest] = order.current;
const elFront = refs[front].current;
const tl = gsap.timeline();
tlRef.current = tl;
tl.to(elFront, {
y: "+=500",
duration: config.durDrop,
ease: config.ease,
});
tl.addLabel("promote", `-=${config.durDrop * config.promoteOverlap}`);
rest.forEach((idx, i) => {
const el = refs[idx].current;
const slot = makeSlot(i, cardDistance, verticalDistance, refs.length);
tl.set(el, { zIndex: slot.zIndex }, "promote");
tl.to(
el,
{
x: slot.x,
y: slot.y,
z: slot.z,
duration: config.durMove,
ease: config.ease,
},
`promote+=${i * 0.15}`,
);
});
const backSlot = makeSlot(refs.length - 1, cardDistance, verticalDistance, refs.length);
tl.addLabel("return", `promote+=${config.durMove * config.returnDelay}`);
tl.call(
() => {
gsap.set(elFront, { zIndex: backSlot.zIndex });
},
undefined,
"return",
);
tl.to(
elFront,
{
x: backSlot.x,
y: backSlot.y,
z: backSlot.z,
duration: config.durReturn,
ease: config.ease,
},
"return",
);
tl.call(() => {
order.current = [...rest, front];
});
};
swap();
intervalRef.current = window.setInterval(swap, delay);
if (pauseOnHover) {
const node = container.current;
const pause = () => {
tlRef.current?.pause();
clearInterval(intervalRef.current);
};
const resume = () => {
tlRef.current?.play();
intervalRef.current = window.setInterval(swap, delay);
};
node.addEventListener("mouseenter", pause);
node.addEventListener("mouseleave", resume);
return () => {
node.removeEventListener("mouseenter", pause);
node.removeEventListener("mouseleave", resume);
clearInterval(intervalRef.current);
};
}
return () => clearInterval(intervalRef.current);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cardDistance, verticalDistance, delay, pauseOnHover, skewAmount, easing]);
const rendered = childArr.map((child, i) =>
isValidElement(child)
? cloneElement(child, {
key: i,
ref: refs[i],
style: { width, height, ...(child.props.style ?? {}) },
onClick: (e) => {
child.props.onClick?.(e);
onCardClick?.(i);
},
})
: child,
);
return (
<div ref={container} className="card-swap-container" style={{ width, height }}>
{rendered}
</div>
);
};
export default CardSwap;

View File

@ -0,0 +1,10 @@
.circular-gallery {
width: 100%;
height: 100%;
overflow: hidden;
cursor: grab;
}
.circular-gallery:active {
cursor: grabbing;
}

View File

@ -0,0 +1,591 @@
import { Camera, Mesh, Plane, Program, Renderer, Texture, Transform } from "ogl";
import { useEffect, useRef } from "react";
import "./CircularGallery.css";
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
function lerp(p1, p2, t) {
return p1 + (p2 - p1) * t;
}
function autoBind(instance) {
const proto = Object.getPrototypeOf(instance);
Object.getOwnPropertyNames(proto).forEach((key) => {
if (key !== "constructor" && typeof instance[key] === "function") {
instance[key] = instance[key].bind(instance);
}
});
}
const DEFAULT_FONT = "bold 30px Figtree";
const DEFAULT_FONT_URL =
"https://fonts.googleapis.com/css2?family=Figtree:wght@400;700&display=swap";
function deriveFontFamilyFromUrl(url) {
const fileName = (url.split("/").pop() || "custom-font").split("?")[0];
const base = fileName.replace(/\.(woff2?|ttf|otf|eot)$/i, "");
return base.replace(/[^a-zA-Z0-9-_ ]/g, "").trim() || "CircularGalleryFont";
}
async function loadFontFromStylesheet(url) {
const response = await fetch(url);
if (!response.ok) throw new Error(`Failed to fetch font stylesheet (${response.status})`);
const cssText = await response.text();
const faceBlocks = cssText.match(/@font-face\s*{[^}]*}/g) || [];
let family = null;
const fontFaces = [];
for (const block of faceBlocks) {
const familyMatch = block.match(/font-family:\s*['"]?([^;'"]+)['"]?/);
const urlMatch = block.match(/url\(\s*['"]?([^'")]+)['"]?\s*\)/);
if (!familyMatch || !urlMatch) continue;
family = familyMatch[1].trim();
const descriptors = {};
const weightMatch = block.match(/font-weight:\s*([^;]+);/);
const styleMatch = block.match(/font-style:\s*([^;]+);/);
const rangeMatch = block.match(/unicode-range:\s*([^;]+);/);
if (weightMatch) descriptors.weight = weightMatch[1].trim();
if (styleMatch) descriptors.style = styleMatch[1].trim();
if (rangeMatch) descriptors.unicodeRange = rangeMatch[1].trim();
fontFaces.push(new FontFace(family, `url(${urlMatch[1]})`, descriptors));
}
if (!family) throw new Error("No @font-face rule found in the stylesheet");
await Promise.allSettled(
fontFaces.map(async (face) => {
await face.load();
document.fonts.add(face);
}),
);
return family;
}
async function loadFontFromFile(url) {
const family = deriveFontFamilyFromUrl(url);
const fontFace = new FontFace(family, `url(${url})`);
await fontFace.load();
document.fonts.add(fontFace);
return family;
}
async function loadCustomFont(fontUrl) {
const isStylesheet = fontUrl.includes("fonts.googleapis.com") || /\.css(\?.*)?$/i.test(fontUrl);
return isStylesheet ? loadFontFromStylesheet(fontUrl) : loadFontFromFile(fontUrl);
}
async function resolveFont(font, fontUrl) {
const effectiveUrl = fontUrl || (font === DEFAULT_FONT ? DEFAULT_FONT_URL : null);
if (!effectiveUrl) {
if (document.fonts && document.fonts.load) {
try {
await document.fonts.load(font);
await document.fonts.ready;
} catch {
/* ignore */
}
}
return font;
}
try {
const family = await loadCustomFont(effectiveUrl);
const sizeMatch = font.match(/^\s*(.*?\d+px)/);
const prefix = sizeMatch ? sizeMatch[1].trim() : "bold 30px";
const resolved = `${prefix} "${family}"`;
if (document.fonts && document.fonts.load) {
try {
await document.fonts.load(resolved);
} catch {
/* ignore */
}
}
return resolved;
} catch (error) {
console.error("CircularGallery: unable to load font from", fontUrl, error);
return font;
}
}
function getFontSize(font) {
const match = font.match(/(\d+)px/);
return match ? parseInt(match[1], 10) : 30;
}
function createTextTexture(gl, text, font = "bold 30px monospace", color = "black") {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
context.font = font;
const metrics = context.measureText(text);
const textWidth = Math.ceil(metrics.width);
const textHeight = Math.ceil(getFontSize(font) * 1.2);
canvas.width = textWidth + 20;
canvas.height = textHeight + 20;
context.font = font;
context.fillStyle = color;
context.textBaseline = "middle";
context.textAlign = "center";
context.clearRect(0, 0, canvas.width, canvas.height);
context.fillText(text, canvas.width / 2, canvas.height / 2);
const texture = new Texture(gl, { generateMipmaps: false });
texture.image = canvas;
return { texture, width: canvas.width, height: canvas.height };
}
class Title {
constructor({ gl, plane, renderer, text, textColor = "#545050", font = "30px sans-serif" }) {
autoBind(this);
this.gl = gl;
this.plane = plane;
this.renderer = renderer;
this.text = text;
this.textColor = textColor;
this.font = font;
this.createMesh();
}
createMesh() {
const { texture, width, height } = createTextTexture(
this.gl,
this.text,
this.font,
this.textColor,
);
const geometry = new Plane(this.gl);
const program = new Program(this.gl, {
vertex: `
attribute vec3 position;
attribute vec2 uv;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragment: `
precision highp float;
uniform sampler2D tMap;
varying vec2 vUv;
void main() {
vec4 color = texture2D(tMap, vUv);
if (color.a < 0.1) discard;
gl_FragColor = color;
}
`,
uniforms: { tMap: { value: texture } },
transparent: true,
});
this.mesh = new Mesh(this.gl, { geometry, program });
const aspect = width / height;
const textHeight = this.plane.scale.y * 0.15;
const textWidth = textHeight * aspect;
this.mesh.scale.set(textWidth, textHeight, 1);
this.mesh.position.y = -this.plane.scale.y * 0.5 - textHeight * 0.5 - 0.05;
this.mesh.setParent(this.plane);
}
}
class Media {
constructor({
geometry,
gl,
image,
index,
length,
renderer,
scene,
screen,
text,
viewport,
bend,
textColor,
borderRadius = 0,
font,
}) {
this.extra = 0;
this.geometry = geometry;
this.gl = gl;
this.image = image;
this.index = index;
this.length = length;
this.renderer = renderer;
this.scene = scene;
this.screen = screen;
this.text = text;
this.viewport = viewport;
this.bend = bend;
this.textColor = textColor;
this.borderRadius = borderRadius;
this.font = font;
this.createShader();
this.createMesh();
this.createTitle();
this.onResize();
}
createShader() {
const texture = new Texture(this.gl, {
generateMipmaps: true,
});
this.program = new Program(this.gl, {
depthTest: false,
depthWrite: false,
vertex: `
precision highp float;
attribute vec3 position;
attribute vec2 uv;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform float uTime;
uniform float uSpeed;
varying vec2 vUv;
void main() {
vUv = uv;
vec3 p = position;
p.z = 0.0; // flat no wave/ripple, cards stand still
gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}
`,
fragment: `
precision highp float;
uniform vec2 uImageSizes;
uniform vec2 uPlaneSizes;
uniform sampler2D tMap;
uniform float uBorderRadius;
varying vec2 vUv;
float roundedBoxSDF(vec2 p, vec2 b, float r) {
vec2 d = abs(p) - b;
return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0) - r;
}
void main() {
vec2 ratio = vec2(
min((uPlaneSizes.x / uPlaneSizes.y) / (uImageSizes.x / uImageSizes.y), 1.0),
min((uPlaneSizes.y / uPlaneSizes.x) / (uImageSizes.y / uImageSizes.x), 1.0)
);
vec2 uv = vec2(
vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,
vUv.y * ratio.y + (1.0 - ratio.y) * 0.5
);
vec4 color = texture2D(tMap, uv);
float d = roundedBoxSDF(vUv - 0.5, vec2(0.5 - uBorderRadius), uBorderRadius);
float edgeSmooth = 0.002;
float alpha = 1.0 - smoothstep(-edgeSmooth, edgeSmooth, d);
gl_FragColor = vec4(color.rgb, alpha);
}
`,
uniforms: {
tMap: { value: texture },
uPlaneSizes: { value: [0, 0] },
uImageSizes: { value: [0, 0] },
uSpeed: { value: 0 },
uTime: { value: 100 * Math.random() },
uBorderRadius: { value: this.borderRadius },
},
transparent: true,
});
const img = new Image();
img.crossOrigin = "anonymous";
img.src = this.image;
img.onload = () => {
texture.image = img;
this.program.uniforms.uImageSizes.value = [img.naturalWidth, img.naturalHeight];
};
}
createMesh() {
this.plane = new Mesh(this.gl, {
geometry: this.geometry,
program: this.program,
});
this.plane.setParent(this.scene);
}
createTitle() {
this.title = new Title({
gl: this.gl,
plane: this.plane,
renderer: this.renderer,
text: this.text,
textColor: this.textColor,
font: this.font,
});
}
update(scroll, direction) {
this.plane.position.x = this.x - scroll.current - this.extra;
const x = this.plane.position.x;
const H = this.viewport.width / 2;
if (this.bend === 0) {
this.plane.position.y = 0;
this.plane.rotation.z = 0;
} else {
const B_abs = Math.abs(this.bend);
const R = (H * H + B_abs * B_abs) / (2 * B_abs);
const effectiveX = Math.min(Math.abs(x), H);
const arc = R - Math.sqrt(R * R - effectiveX * effectiveX);
if (this.bend > 0) {
this.plane.position.y = -arc;
this.plane.rotation.z = -Math.sign(x) * Math.asin(effectiveX / R);
} else {
this.plane.position.y = arc;
this.plane.rotation.z = Math.sign(x) * Math.asin(effectiveX / R);
}
}
this.speed = scroll.current - scroll.last;
this.program.uniforms.uTime.value += 0.04;
this.program.uniforms.uSpeed.value = this.speed;
const planeOffset = this.plane.scale.x / 2;
const viewportOffset = this.viewport.width / 2;
this.isBefore = this.plane.position.x + planeOffset < -viewportOffset;
this.isAfter = this.plane.position.x - planeOffset > viewportOffset;
if (direction === "right" && this.isBefore) {
this.extra -= this.widthTotal;
this.isBefore = this.isAfter = false;
}
if (direction === "left" && this.isAfter) {
this.extra += this.widthTotal;
this.isBefore = this.isAfter = false;
}
}
onResize({ screen, viewport } = {}) {
if (screen) this.screen = screen;
if (viewport) {
this.viewport = viewport;
if (this.plane.program.uniforms.uViewportSizes) {
this.plane.program.uniforms.uViewportSizes.value = [
this.viewport.width,
this.viewport.height,
];
}
}
this.scale = this.screen.height / 1500;
this.plane.scale.y = (this.viewport.height * (900 * this.scale)) / this.screen.height;
this.plane.scale.x = (this.viewport.width * (700 * this.scale)) / this.screen.width;
this.plane.program.uniforms.uPlaneSizes.value = [this.plane.scale.x, this.plane.scale.y];
this.padding = 2;
this.width = this.plane.scale.x + this.padding;
this.widthTotal = this.width * this.length;
this.x = this.width * this.index;
}
}
class App {
constructor(
container,
{
items,
bend,
textColor = "#ffffff",
borderRadius = 0,
font = "bold 30px Figtree",
scrollSpeed = 2,
scrollEase = 0.05,
} = {},
) {
document.documentElement.classList.remove("no-js");
this.container = container;
this.scrollSpeed = scrollSpeed;
this.scroll = { ease: scrollEase, current: 0, target: 0, last: 0 };
this.onCheckDebounce = debounce(this.onCheck, 200);
this.createRenderer();
this.createCamera();
this.createScene();
this.onResize();
this.createGeometry();
this.createMedias(items, bend, textColor, borderRadius, font);
this.update();
this.addEventListeners();
}
createRenderer() {
this.renderer = new Renderer({
alpha: true,
antialias: true,
dpr: Math.min(window.devicePixelRatio || 1, 2),
});
this.gl = this.renderer.gl;
this.gl.clearColor(0, 0, 0, 0);
this.container.appendChild(this.gl.canvas);
}
createCamera() {
this.camera = new Camera(this.gl);
this.camera.fov = 45;
this.camera.position.z = 20;
}
createScene() {
this.scene = new Transform();
}
createGeometry() {
this.planeGeometry = new Plane(this.gl, {
heightSegments: 50,
widthSegments: 100,
});
}
createMedias(items, bend = 1, textColor, borderRadius, font) {
const defaultItems = [
{ image: `https://picsum.photos/seed/1/800/600?grayscale`, text: "Bridge" },
{ image: `https://picsum.photos/seed/2/800/600?grayscale`, text: "Desk Setup" },
{ image: `https://picsum.photos/seed/3/800/600?grayscale`, text: "Waterfall" },
{ image: `https://picsum.photos/seed/4/800/600?grayscale`, text: "Strawberries" },
{ image: `https://picsum.photos/seed/5/800/600?grayscale`, text: "Deep Diving" },
{ image: `https://picsum.photos/seed/16/800/600?grayscale`, text: "Train Track" },
{ image: `https://picsum.photos/seed/17/800/600?grayscale`, text: "Santorini" },
{ image: `https://picsum.photos/seed/8/800/600?grayscale`, text: "Blurry Lights" },
{ image: `https://picsum.photos/seed/9/800/600?grayscale`, text: "New York" },
{ image: `https://picsum.photos/seed/10/800/600?grayscale`, text: "Good Boy" },
{ image: `https://picsum.photos/seed/21/800/600?grayscale`, text: "Coastline" },
{ image: `https://picsum.photos/seed/12/800/600?grayscale`, text: "Palm Trees" },
];
const galleryItems = items && items.length ? items : defaultItems;
this.mediasImages = galleryItems.concat(galleryItems);
this.medias = this.mediasImages.map((data, index) => {
return new Media({
geometry: this.planeGeometry,
gl: this.gl,
image: data.image,
index,
length: this.mediasImages.length,
renderer: this.renderer,
scene: this.scene,
screen: this.screen,
text: data.text,
viewport: this.viewport,
bend,
textColor,
borderRadius,
font,
});
});
}
onTouchDown(e) {
this.isDown = true;
this.scroll.position = this.scroll.current;
this.start = e.touches ? e.touches[0].clientX : e.clientX;
}
onTouchMove(e) {
if (!this.isDown) return;
const x = e.touches ? e.touches[0].clientX : e.clientX;
const distance = (this.start - x) * (this.scrollSpeed * 0.025);
this.scroll.target = this.scroll.position + distance;
}
onTouchUp() {
this.isDown = false;
this.onCheck();
}
onWheel(e) {
const delta = e.deltaY || e.wheelDelta || e.detail;
this.scroll.target += (delta > 0 ? this.scrollSpeed : -this.scrollSpeed) * 0.2;
this.onCheckDebounce();
}
onCheck() {
if (!this.medias || !this.medias[0]) return;
const width = this.medias[0].width;
const itemIndex = Math.round(Math.abs(this.scroll.target) / width);
const item = width * itemIndex;
this.scroll.target = this.scroll.target < 0 ? -item : item;
}
onResize() {
this.screen = {
width: this.container.clientWidth,
height: this.container.clientHeight,
};
this.renderer.setSize(this.screen.width, this.screen.height);
this.camera.perspective({
aspect: this.screen.width / this.screen.height,
});
const fov = (this.camera.fov * Math.PI) / 180;
const height = 2 * Math.tan(fov / 2) * this.camera.position.z;
const width = height * this.camera.aspect;
this.viewport = { width, height };
if (this.medias) {
this.medias.forEach((media) =>
media.onResize({ screen: this.screen, viewport: this.viewport }),
);
}
}
update() {
this.scroll.current = lerp(this.scroll.current, this.scroll.target, this.scroll.ease);
const direction = this.scroll.current > this.scroll.last ? "right" : "left";
if (this.medias) {
this.medias.forEach((media) => media.update(this.scroll, direction));
}
this.renderer.render({ scene: this.scene, camera: this.camera });
this.scroll.last = this.scroll.current;
this.raf = window.requestAnimationFrame(this.update.bind(this));
}
addEventListeners() {
this.boundOnResize = this.onResize.bind(this);
this.boundOnWheel = this.onWheel.bind(this);
this.boundOnTouchDown = this.onTouchDown.bind(this);
this.boundOnTouchMove = this.onTouchMove.bind(this);
this.boundOnTouchUp = this.onTouchUp.bind(this);
window.addEventListener("resize", this.boundOnResize);
window.addEventListener("mousewheel", this.boundOnWheel);
window.addEventListener("wheel", this.boundOnWheel);
window.addEventListener("mousedown", this.boundOnTouchDown);
window.addEventListener("mousemove", this.boundOnTouchMove);
window.addEventListener("mouseup", this.boundOnTouchUp);
window.addEventListener("touchstart", this.boundOnTouchDown);
window.addEventListener("touchmove", this.boundOnTouchMove);
window.addEventListener("touchend", this.boundOnTouchUp);
}
destroy() {
window.cancelAnimationFrame(this.raf);
window.removeEventListener("resize", this.boundOnResize);
window.removeEventListener("mousewheel", this.boundOnWheel);
window.removeEventListener("wheel", this.boundOnWheel);
window.removeEventListener("mousedown", this.boundOnTouchDown);
window.removeEventListener("mousemove", this.boundOnTouchMove);
window.removeEventListener("mouseup", this.boundOnTouchUp);
window.removeEventListener("touchstart", this.boundOnTouchDown);
window.removeEventListener("touchmove", this.boundOnTouchMove);
window.removeEventListener("touchend", this.boundOnTouchUp);
if (this.renderer && this.renderer.gl && this.renderer.gl.canvas.parentNode) {
this.renderer.gl.canvas.parentNode.removeChild(this.renderer.gl.canvas);
}
}
}
export default function CircularGallery({
items,
bend = 3,
textColor = "#ffffff",
borderRadius = 0.05,
font = "bold 30px Figtree",
fontUrl,
scrollSpeed = 2,
scrollEase = 0.05,
}) {
const containerRef = useRef(null);
useEffect(() => {
if (!containerRef.current) return;
let app;
let isMounted = true;
resolveFont(font, fontUrl).then((resolvedFont) => {
if (!isMounted || !containerRef.current) return;
app = new App(containerRef.current, {
items,
bend,
textColor,
borderRadius,
font: resolvedFont,
scrollSpeed,
scrollEase,
});
});
return () => {
isMounted = false;
if (app) app.destroy();
};
}, [items, bend, textColor, borderRadius, font, fontUrl, scrollSpeed, scrollEase]);
return <div className="circular-gallery" ref={containerRef} />;
}

View File

@ -0,0 +1,6 @@
.laser-flow-container {
width: 100%;
height: 100%;
position: relative;
pointer-events: none;
}

View File

@ -0,0 +1,584 @@
import { useEffect, useRef } from 'react';
import * as THREE from 'three';
import './LaserFlow.css';
const VERT = `
precision highp float;
attribute vec3 position;
void main(){
gl_Position = vec4(position, 1.0);
}
`;
const FRAG = `
#ifdef GL_ES
#extension GL_OES_standard_derivatives : enable
#endif
precision highp float;
precision mediump int;
uniform float iTime;
uniform vec3 iResolution;
uniform vec4 iMouse;
uniform float uWispDensity;
uniform float uTiltScale;
uniform float uFlowTime;
uniform float uFogTime;
uniform float uBeamXFrac;
uniform float uBeamYFrac;
uniform float uFlowSpeed;
uniform float uVLenFactor;
uniform float uHLenFactor;
uniform float uFogIntensity;
uniform float uFogScale;
uniform float uWSpeed;
uniform float uWIntensity;
uniform float uFlowStrength;
uniform float uDecay;
uniform float uFalloffStart;
uniform float uFogFallSpeed;
uniform vec3 uColor;
uniform float uFade;
// Core beam/flare shaping and dynamics
#define PI 3.14159265359
#define TWO_PI 6.28318530718
#define EPS 1e-6
#define EDGE_SOFT (DT_LOCAL*4.0)
#define DT_LOCAL 0.0038
#define TAP_RADIUS 6
#define R_H 150.0
#define R_V 150.0
#define FLARE_HEIGHT 16.0
#define FLARE_AMOUNT 8.0
#define FLARE_EXP 2.0
#define TOP_FADE_START 0.1
#define TOP_FADE_EXP 1.0
#define FLOW_PERIOD 0.5
#define FLOW_SHARPNESS 1.5
// Wisps (animated micro-streaks) that travel along the beam
#define W_BASE_X 1.5
#define W_LAYER_GAP 0.25
#define W_LANES 10
#define W_SIDE_DECAY 0.5
#define W_HALF 0.01
#define W_AA 0.15
#define W_CELL 20.0
#define W_SEG_MIN 0.01
#define W_SEG_MAX 0.55
#define W_CURVE_AMOUNT 15.0
#define W_CURVE_RANGE (FLARE_HEIGHT - 3.0)
#define W_BOTTOM_EXP 10.0
// Volumetric fog controls
#define FOG_ON 1
#define FOG_CONTRAST 1.2
#define FOG_SPEED_U 0.1
#define FOG_SPEED_V -0.1
#define FOG_OCTAVES 5
#define FOG_BOTTOM_BIAS 0.8
#define FOG_TILT_TO_MOUSE 0.05
#define FOG_TILT_DEADZONE 0.01
#define FOG_TILT_MAX_X 0.35
#define FOG_TILT_SHAPE 1.5
#define FOG_BEAM_MIN 0.0
#define FOG_BEAM_MAX 0.75
#define FOG_MASK_GAMMA 0.5
#define FOG_EXPAND_SHAPE 12.2
#define FOG_EDGE_MIX 0.5
// Horizontal vignette for the fog volume
#define HFOG_EDGE_START 0.20
#define HFOG_EDGE_END 0.98
#define HFOG_EDGE_GAMMA 1.4
#define HFOG_Y_RADIUS 25.0
#define HFOG_Y_SOFT 60.0
// Beam extents and edge masking
#define EDGE_X0 0.22
#define EDGE_X1 0.995
#define EDGE_X_GAMMA 1.25
#define EDGE_LUMA_T0 0.0
#define EDGE_LUMA_T1 2.0
#define DITHER_STRENGTH 1.0
float g(float x){return x<=0.00031308?12.92*x:1.055*pow(x,1.0/2.4)-0.055;}
float bs(vec2 p,vec2 q,float powr){
float d=distance(p,q),f=powr*uFalloffStart,r=(f*f)/(d*d+EPS);
return powr*min(1.0,r);
}
float bsa(vec2 p,vec2 q,float powr,vec2 s){
vec2 d=p-q; float dd=(d.x*d.x)/(s.x*s.x)+(d.y*d.y)/(s.y*s.y),f=powr*uFalloffStart,r=(f*f)/(dd+EPS);
return powr*min(1.0,r);
}
float tri01(float x){float f=fract(x);return 1.0-abs(f*2.0-1.0);}
float tauWf(float t,float tmin,float tmax){float a=smoothstep(tmin,tmin+EDGE_SOFT,t),b=1.0-smoothstep(tmax-EDGE_SOFT,tmax,t);return max(0.0,a*b);}
float h21(vec2 p){p=fract(p*vec2(123.34,456.21));p+=dot(p,p+34.123);return fract(p.x*p.y);}
float vnoise(vec2 p){
vec2 i=floor(p),f=fract(p);
float a=h21(i),b=h21(i+vec2(1,0)),c=h21(i+vec2(0,1)),d=h21(i+vec2(1,1));
vec2 u=f*f*(3.0-2.0*f);
return mix(mix(a,b,u.x),mix(c,d,u.x),u.y);
}
float fbm2(vec2 p){
float v=0.0,amp=0.6; mat2 m=mat2(0.86,0.5,-0.5,0.86);
for(int i=0;i<FOG_OCTAVES;++i){v+=amp*vnoise(p); p=m*p*2.03+17.1; amp*=0.52;}
return v;
}
float rGate(float x,float l){float a=smoothstep(0.0,W_AA,x),b=1.0-smoothstep(l,l+W_AA,x);return max(0.0,a*b);}
float flareY(float y){float t=clamp(1.0-(clamp(y,0.0,FLARE_HEIGHT)/max(FLARE_HEIGHT,EPS)),0.0,1.0);return pow(t,FLARE_EXP);}
float vWisps(vec2 uv,float topF){
float y=uv.y,yf=(y+uFlowTime*uWSpeed)/W_CELL;
float dRaw=clamp(uWispDensity,0.0,2.0),d=dRaw<=0.0?1.0:dRaw;
float lanesF=floor(float(W_LANES)*min(d,1.0)+0.5); // WebGL1-safe
int lanes=int(max(1.0,lanesF));
float sp=min(d,1.0),ep=max(d-1.0,0.0);
float fm=flareY(max(y,0.0)),rm=clamp(1.0-(y/max(W_CURVE_RANGE,EPS)),0.0,1.0),cm=fm*rm;
const float G=0.05; float xS=1.0+(FLARE_AMOUNT*W_CURVE_AMOUNT*G)*cm;
float sPix=clamp(y/R_V,0.0,1.0),bGain=pow(1.0-sPix,W_BOTTOM_EXP),sum=0.0;
for(int s=0;s<2;++s){
float sgn=s==0?-1.0:1.0;
for(int i=0;i<W_LANES;++i){
if(i>=lanes) break;
float off=W_BASE_X+float(i)*W_LAYER_GAP,xc=sgn*(off*xS);
float dx=abs(uv.x-xc),lat=1.0-smoothstep(W_HALF,W_HALF+W_AA,dx),amp=exp(-off*W_SIDE_DECAY);
float seed=h21(vec2(off,sgn*17.0)),yf2=yf+seed*7.0,ci=floor(yf2),fy=fract(yf2);
float seg=mix(W_SEG_MIN,W_SEG_MAX,h21(vec2(ci,off*2.3)));
float spR=h21(vec2(ci,off+sgn*31.0)),seg1=rGate(fy,seg)*step(spR,sp);
if(ep>0.0){float spR2=h21(vec2(ci*3.1+7.0,off*5.3+sgn*13.0)); float f2=fract(fy+0.5); seg1+=rGate(f2,seg*0.9)*step(spR2,ep);}
sum+=amp*lat*seg1;
}
}
float span=smoothstep(-3.0,0.0,y)*(1.0-smoothstep(R_V-6.0,R_V,y));
return uWIntensity*sum*topF*bGain*span;
}
void mainImage(out vec4 fc,in vec2 frag){
vec2 C=iResolution.xy*.5; float invW=1.0/max(C.x,1.0);
vec2 sc=(512.0/iResolution.xy)*.4;
vec2 uv=(frag-C)*sc,off=vec2(uBeamXFrac*iResolution.x*sc.x,uBeamYFrac*iResolution.y*sc.y);
vec2 uvc = uv - off;
float a=0.0,b=0.0;
float basePhase=1.5*PI+uDecay*.5; float tauMin=basePhase-uDecay; float tauMax=basePhase;
float cx=clamp(uvc.x/(R_H*uHLenFactor),-1.0,1.0),tH=clamp(TWO_PI-acos(cx),tauMin,tauMax);
for(int k=-TAP_RADIUS;k<=TAP_RADIUS;++k){
float tu=tH+float(k)*DT_LOCAL,wt=tauWf(tu,tauMin,tauMax); if(wt<=0.0) continue;
float spd=max(abs(sin(tu)),0.02),u=clamp((basePhase-tu)/max(uDecay,EPS),0.0,1.0),env=pow(1.0-abs(u*2.0-1.0),0.8);
vec2 p=vec2((R_H*uHLenFactor)*cos(tu),0.0);
a+=wt*bs(uvc,p,env*spd);
}
float yPix=uvc.y,cy=clamp(-yPix/(R_V*uVLenFactor),-1.0,1.0),tV=clamp(TWO_PI-acos(cy),tauMin,tauMax);
for(int k=-TAP_RADIUS;k<=TAP_RADIUS;++k){
float tu=tV+float(k)*DT_LOCAL,wt=tauWf(tu,tauMin,tauMax); if(wt<=0.0) continue;
float yb=(-R_V)*cos(tu),s=clamp(yb/R_V,0.0,1.0),spd=max(abs(sin(tu)),0.02);
float env=pow(1.0-s,0.6)*spd;
float cap=1.0-smoothstep(TOP_FADE_START,1.0,s); cap=pow(cap,TOP_FADE_EXP); env*=cap;
float ph=s/max(FLOW_PERIOD,EPS)+uFlowTime*uFlowSpeed;
float fl=pow(tri01(ph),FLOW_SHARPNESS);
env*=mix(1.0-uFlowStrength,1.0,fl);
float yp=(-R_V*uVLenFactor)*cos(tu),m=pow(smoothstep(FLARE_HEIGHT,0.0,yp),FLARE_EXP),wx=1.0+FLARE_AMOUNT*m;
vec2 sig=vec2(wx,1.0),p=vec2(0.0,yp);
float mask=step(0.0,yp);
b+=wt*bsa(uvc,p,mask*env,sig);
}
float sPix=clamp(yPix/R_V,0.0,1.0),topA=pow(1.0-smoothstep(TOP_FADE_START,1.0,sPix),TOP_FADE_EXP);
float L=a+b*topA;
float w=vWisps(vec2(uvc.x,yPix),topA);
float fog=0.0;
#if FOG_ON
vec2 fuv=uvc*uFogScale;
float mAct=step(1.0,length(iMouse.xy)),nx=((iMouse.x-C.x)*invW)*mAct;
float ax = abs(nx);
float stMag = mix(ax, pow(ax, FOG_TILT_SHAPE), 0.35);
float st = sign(nx) * stMag * uTiltScale;
st = clamp(st, -FOG_TILT_MAX_X, FOG_TILT_MAX_X);
vec2 dir=normalize(vec2(st,1.0));
fuv+=uFogTime*uFogFallSpeed*dir;
vec2 prp=vec2(-dir.y,dir.x);
fuv+=prp*(0.08*sin(dot(uvc,prp)*0.08+uFogTime*0.9));
float n=fbm2(fuv+vec2(fbm2(fuv+vec2(7.3,2.1)),fbm2(fuv+vec2(-3.7,5.9)))*0.6);
n=pow(clamp(n,0.0,1.0),FOG_CONTRAST);
float pixW = 1.0 / max(iResolution.y, 1.0);
#ifdef GL_OES_standard_derivatives
float wL = max(fwidth(L), pixW);
#else
float wL = pixW;
#endif
float m0=pow(smoothstep(FOG_BEAM_MIN - wL, FOG_BEAM_MAX + wL, L),FOG_MASK_GAMMA);
float bm=1.0-pow(1.0-m0,FOG_EXPAND_SHAPE); bm=mix(bm*m0,bm,FOG_EDGE_MIX);
float yP=1.0-smoothstep(HFOG_Y_RADIUS,HFOG_Y_RADIUS+HFOG_Y_SOFT,abs(yPix));
float nxF=abs((frag.x-C.x)*invW),hE=1.0-smoothstep(HFOG_EDGE_START,HFOG_EDGE_END,nxF); hE=pow(clamp(hE,0.0,1.0),HFOG_EDGE_GAMMA);
float hW=mix(1.0,hE,clamp(yP,0.0,1.0));
float bBias=mix(1.0,1.0-sPix,FOG_BOTTOM_BIAS);
float browserFogIntensity = uFogIntensity;
browserFogIntensity *= 1.8;
float radialFade = 1.0 - smoothstep(0.0, 0.7, length(uvc) / 120.0);
float safariFog = n * browserFogIntensity * bBias * bm * hW * radialFade;
fog = safariFog;
#endif
float LF=L+fog;
float dith=(h21(frag)-0.5)*(DITHER_STRENGTH/255.0);
float tone=g(LF+w);
vec3 col=tone*uColor+dith;
float alpha=clamp(g(L+w*0.6)+dith*0.6,0.0,1.0);
float nxE=abs((frag.x-C.x)*invW),xF=pow(clamp(1.0-smoothstep(EDGE_X0,EDGE_X1,nxE),0.0,1.0),EDGE_X_GAMMA);
float scene=LF+max(0.0,w)*0.5,hi=smoothstep(EDGE_LUMA_T0,EDGE_LUMA_T1,scene);
float eM=mix(xF,1.0,hi);
col*=eM; alpha*=eM;
col*=uFade; alpha*=uFade;
fc=vec4(col,alpha);
}
void main(){
vec4 fc;
mainImage(fc, gl_FragCoord.xy);
gl_FragColor = fc;
}
`;
export const LaserFlow = ({
className,
style,
wispDensity = 1,
dpr,
mouseSmoothTime = 0.0,
mouseTiltStrength = 0.01,
horizontalBeamOffset = 0.1,
verticalBeamOffset = 0.0,
flowSpeed = 0.35,
verticalSizing = 2.0,
horizontalSizing = 0.5,
fogIntensity = 0.45,
fogScale = 0.3,
wispSpeed = 15.0,
wispIntensity = 5.0,
flowStrength = 0.25,
decay = 1.1,
falloffStart = 1.2,
fogFallSpeed = 0.6,
color = '#FF79C6'
}) => {
const mountRef = useRef(null);
const rendererRef = useRef(null);
const uniformsRef = useRef(null);
const hasFadedRef = useRef(false);
const rectRef = useRef(null);
const baseDprRef = useRef(1);
const currentDprRef = useRef(1);
const lastSizeRef = useRef({ width: 0, height: 0, dpr: 0 });
const fpsSamplesRef = useRef([]);
const lastFpsCheckRef = useRef(performance.now());
const emaDtRef = useRef(16.7);
const pausedRef = useRef(false);
const inViewRef = useRef(true);
const hexToRGB = hex => {
let c = hex.trim();
if (c[0] === '#') c = c.slice(1);
if (c.length === 3)
c = c
.split('')
.map(x => x + x)
.join('');
const n = parseInt(c, 16) || 0xffffff;
return { r: ((n >> 16) & 255) / 255, g: ((n >> 8) & 255) / 255, b: (n & 255) / 255 };
};
useEffect(() => {
const mount = mountRef.current;
const renderer = new THREE.WebGLRenderer({
antialias: false,
alpha: false,
depth: false,
stencil: false,
powerPreference: 'high-performance',
premultipliedAlpha: false,
preserveDrawingBuffer: false,
failIfMajorPerformanceCaveat: false,
logarithmicDepthBuffer: false
});
rendererRef.current = renderer;
baseDprRef.current = Math.min(dpr ?? (window.devicePixelRatio || 1), 2);
currentDprRef.current = baseDprRef.current;
renderer.setPixelRatio(currentDprRef.current);
renderer.shadowMap.enabled = false;
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.setClearColor(0x000000, 1);
const canvas = renderer.domElement;
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.display = 'block';
mount.appendChild(canvas);
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array([-1, -1, 0, 3, -1, 0, -1, 3, 0]), 3));
const uniforms = {
iTime: { value: 0 },
iResolution: { value: new THREE.Vector3(1, 1, 1) },
iMouse: { value: new THREE.Vector4(0, 0, 0, 0) },
uWispDensity: { value: wispDensity },
uTiltScale: { value: mouseTiltStrength },
uFlowTime: { value: 0 },
uFogTime: { value: 0 },
uBeamXFrac: { value: horizontalBeamOffset },
uBeamYFrac: { value: verticalBeamOffset },
uFlowSpeed: { value: flowSpeed },
uVLenFactor: { value: verticalSizing },
uHLenFactor: { value: horizontalSizing },
uFogIntensity: { value: fogIntensity },
uFogScale: { value: fogScale },
uWSpeed: { value: wispSpeed },
uWIntensity: { value: wispIntensity },
uFlowStrength: { value: flowStrength },
uDecay: { value: decay },
uFalloffStart: { value: falloffStart },
uFogFallSpeed: { value: fogFallSpeed },
uColor: { value: new THREE.Vector3(1, 1, 1) },
uFade: { value: hasFadedRef.current ? 1 : 0 }
};
uniformsRef.current = uniforms;
const material = new THREE.RawShaderMaterial({
vertexShader: VERT,
fragmentShader: FRAG,
uniforms,
transparent: false,
depthTest: false,
depthWrite: false,
blending: THREE.NormalBlending
});
const mesh = new THREE.Mesh(geometry, material);
mesh.frustumCulled = false;
scene.add(mesh);
const clock = new THREE.Clock();
let prevTime = 0;
let fade = hasFadedRef.current ? 1 : 0;
const mouseTarget = new THREE.Vector2(0, 0);
const mouseSmooth = new THREE.Vector2(0, 0);
const setSizeNow = () => {
const w = mount.clientWidth || 1;
const h = mount.clientHeight || 1;
const pr = currentDprRef.current;
const last = lastSizeRef.current;
const sizeChanged = Math.abs(w - last.width) > 0.5 || Math.abs(h - last.height) > 0.5;
const dprChanged = Math.abs(pr - last.dpr) > 0.01;
if (!sizeChanged && !dprChanged) {
return;
}
lastSizeRef.current = { width: w, height: h, dpr: pr };
renderer.setPixelRatio(pr);
renderer.setSize(w, h, false);
uniforms.iResolution.value.set(w * pr, h * pr, pr);
rectRef.current = canvas.getBoundingClientRect();
if (!pausedRef.current) {
renderer.render(scene, camera);
}
};
let resizeRaf = 0;
const scheduleResize = () => {
if (resizeRaf) cancelAnimationFrame(resizeRaf);
resizeRaf = requestAnimationFrame(setSizeNow);
};
setSizeNow();
const ro = new ResizeObserver(scheduleResize);
ro.observe(mount);
const io = new IntersectionObserver(
entries => {
inViewRef.current = entries[0]?.isIntersecting ?? true;
},
{ root: null, threshold: 0 }
);
io.observe(mount);
const onVis = () => {
pausedRef.current = document.hidden;
};
document.addEventListener('visibilitychange', onVis, { passive: true });
const updateMouse = (clientX, clientY) => {
const rect = rectRef.current;
if (!rect) return;
const x = clientX - rect.left;
const y = clientY - rect.top;
const ratio = currentDprRef.current;
const hb = rect.height * ratio;
mouseTarget.set(x * ratio, hb - y * ratio);
};
const onMove = ev => updateMouse(ev.clientX, ev.clientY);
const onLeave = () => mouseTarget.set(0, 0);
canvas.addEventListener('pointermove', onMove, { passive: true });
canvas.addEventListener('pointerdown', onMove, { passive: true });
canvas.addEventListener('pointerenter', onMove, { passive: true });
canvas.addEventListener('pointerleave', onLeave, { passive: true });
const onCtxLost = e => {
e.preventDefault();
pausedRef.current = true;
};
const onCtxRestored = () => {
pausedRef.current = false;
scheduleResize();
};
canvas.addEventListener('webglcontextlost', onCtxLost, false);
canvas.addEventListener('webglcontextrestored', onCtxRestored, false);
let raf = 0;
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const dprFloor = 0.6;
const lowerThresh = 50;
const upperThresh = 58;
let lastDprChangeRef = 0;
const dprChangeCooldown = 2000;
const adjustDprIfNeeded = now => {
const elapsed = now - lastFpsCheckRef.current;
if (elapsed < 750) return;
const samples = fpsSamplesRef.current;
if (samples.length === 0) {
lastFpsCheckRef.current = now;
return;
}
const avgFps = samples.reduce((a, b) => a + b, 0) / samples.length;
let next = currentDprRef.current;
const base = baseDprRef.current;
if (avgFps < lowerThresh) {
next = clamp(currentDprRef.current * 0.85, dprFloor, base);
} else if (avgFps > upperThresh && currentDprRef.current < base) {
next = clamp(currentDprRef.current * 1.1, dprFloor, base);
}
if (Math.abs(next - currentDprRef.current) > 0.01 && now - lastDprChangeRef > dprChangeCooldown) {
currentDprRef.current = next;
lastDprChangeRef = now;
setSizeNow();
}
fpsSamplesRef.current = [];
lastFpsCheckRef.current = now;
};
const animate = () => {
raf = requestAnimationFrame(animate);
if (pausedRef.current || !inViewRef.current) return;
const t = clock.getElapsedTime();
const dt = Math.max(0, t - prevTime);
prevTime = t;
const dtMs = dt * 1000;
emaDtRef.current = emaDtRef.current * 0.9 + dtMs * 0.1;
const instFps = 1000 / Math.max(1, emaDtRef.current);
fpsSamplesRef.current.push(instFps);
uniforms.iTime.value = t;
const cdt = Math.min(0.033, Math.max(0.001, dt));
uniforms.uFlowTime.value += cdt;
uniforms.uFogTime.value += cdt;
if (!hasFadedRef.current) {
const fadeDur = 1.0;
fade = Math.min(1, fade + cdt / fadeDur);
uniforms.uFade.value = fade;
if (fade >= 1) hasFadedRef.current = true;
}
const tau = Math.max(1e-3, mouseSmoothTime);
const alpha = 1 - Math.exp(-cdt / tau);
mouseSmooth.lerp(mouseTarget, alpha);
uniforms.iMouse.value.set(mouseSmooth.x, mouseSmooth.y, 0, 0);
renderer.render(scene, camera);
adjustDprIfNeeded(performance.now());
};
animate();
return () => {
cancelAnimationFrame(raf);
ro.disconnect();
io.disconnect();
document.removeEventListener('visibilitychange', onVis);
canvas.removeEventListener('pointermove', onMove);
canvas.removeEventListener('pointerdown', onMove);
canvas.removeEventListener('pointerenter', onMove);
canvas.removeEventListener('pointerleave', onLeave);
canvas.removeEventListener('webglcontextlost', onCtxLost);
canvas.removeEventListener('webglcontextrestored', onCtxRestored);
geometry.dispose();
material.dispose();
renderer.dispose();
renderer.forceContextLoss();
if (mount.contains(canvas)) mount.removeChild(canvas);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dpr]);
useEffect(() => {
const uniforms = uniformsRef.current;
if (!uniforms) return;
uniforms.uWispDensity.value = wispDensity;
uniforms.uTiltScale.value = mouseTiltStrength;
uniforms.uBeamXFrac.value = horizontalBeamOffset;
uniforms.uBeamYFrac.value = verticalBeamOffset;
uniforms.uFlowSpeed.value = flowSpeed;
uniforms.uVLenFactor.value = verticalSizing;
uniforms.uHLenFactor.value = horizontalSizing;
uniforms.uFogIntensity.value = fogIntensity;
uniforms.uFogScale.value = fogScale;
uniforms.uWSpeed.value = wispSpeed;
uniforms.uWIntensity.value = wispIntensity;
uniforms.uFlowStrength.value = flowStrength;
uniforms.uDecay.value = decay;
uniforms.uFalloffStart.value = falloffStart;
uniforms.uFogFallSpeed.value = fogFallSpeed;
const { r, g, b } = hexToRGB(color || '#FFFFFF');
uniforms.uColor.value.set(r, g, b);
}, [
wispDensity,
mouseTiltStrength,
horizontalBeamOffset,
verticalBeamOffset,
flowSpeed,
verticalSizing,
horizontalSizing,
fogIntensity,
fogScale,
wispSpeed,
wispIntensity,
flowStrength,
decay,
falloffStart,
fogFallSpeed,
color
]);
return <div ref={mountRef} className={`laser-flow-container ${className || ''}`} style={style} />;
};
export default LaserFlow;

View File

@ -0,0 +1,87 @@
// @ts-expect-error - react-bits JS component (ships without type declarations)
import CircularGallery from "@/components/CircularGallery";
// Real lead photos of each metal from Wikimedia Commons (accurate, stable).
// ponytail: external host (upload.wikimedia.org). If blocked in Iran, bundle under public/.
const METALS = [
{
image:
"https://upload.wikimedia.org/wikipedia/commons/thumb/5/5d/%D0%90%D1%80%D0%BC%D0%B0%D1%82%D1%83%D1%80%D0%BD%D1%8B%D0%B9_%D0%BF%D1%80%D0%BE%D0%BA%D0%B0%D1%82.jpg/960px-%D0%90%D1%80%D0%BC%D0%B0%D1%82%D1%83%D1%80%D0%BD%D1%8B%D0%B9_%D0%BF%D1%80%D0%BE%D0%BA%D0%B0%D1%82.jpg",
text: "فولاد",
},
{
image:
"https://upload.wikimedia.org/wikipedia/commons/thumb/f/f0/NatCopper.jpg/960px-NatCopper.jpg",
text: "مس",
},
{
image: "https://upload.wikimedia.org/wikipedia/commons/5/5d/Aluminium-4.jpg",
text: "آلومینیوم",
},
{
image:
"https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/Nickel_electrolytic_and_1cm3_cube.jpg/960px-Nickel_electrolytic_and_1cm3_cube.jpg",
text: "نیکل",
},
{
image:
"https://upload.wikimedia.org/wikipedia/commons/thumb/f/f9/Zinc_fragment_sublimed_and_1cm3_cube.jpg/960px-Zinc_fragment_sublimed_and_1cm3_cube.jpg",
text: "روی",
},
{
image:
"https://upload.wikimedia.org/wikipedia/commons/thumb/e/e6/Lead_electrolytic_and_1cm3_cube.jpg/960px-Lead_electrolytic_and_1cm3_cube.jpg",
text: "سرب",
},
{
image:
"https://upload.wikimedia.org/wikipedia/commons/thumb/2/2b/Sn-Alpha-Beta.jpg/960px-Sn-Alpha-Beta.jpg",
text: "قلع",
},
{ image: "https://upload.wikimedia.org/wikipedia/commons/2/2e/HematitaEZ.jpg", text: "سنگ‌آهن" },
{
image:
"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d7/Gold-crystals.jpg/960px-Gold-crystals.jpg",
text: "طلا",
},
{
image:
"https://upload.wikimedia.org/wikipedia/commons/thumb/5/55/Silver_crystal.jpg/960px-Silver_crystal.jpg",
text: "نقره",
},
{
image:
"https://upload.wikimedia.org/wikipedia/commons/thumb/5/51/Ferrosilicon.JPG/960px-Ferrosilicon.JPG",
text: "فروسیلیس",
},
{
image:
"https://upload.wikimedia.org/wikipedia/commons/thumb/f/fe/A_bunch_of_rebar.jpg/960px-A_bunch_of_rebar.jpg",
text: "میلگرد",
},
];
export function MetalsGallery() {
return (
<section className="bg-background py-16 sm:py-20">
<div className="mx-auto mb-4 max-w-6xl px-5 text-center">
<h2 className="text-3xl font-bold sm:text-4xl">محصولات فولادی و فلزات</h2>
<p className="mt-3 text-muted-foreground">
دهها فلز و محصول فولادی که قیمتشان را دنبال میکنیم بکش و بچرخان.
</p>
</div>
<div className="relative h-[480px] w-full">
<CircularGallery
items={METALS}
bend={2}
textColor="#1d2330"
borderRadius={0.06}
scrollEase={0.05}
scrollSpeed={2}
font="bold 28px Vazirmatn"
fontUrl="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@700&display=swap"
/>
</div>
</section>
);
}

View File

@ -0,0 +1,97 @@
import { FileText } from "lucide-react";
// @ts-expect-error - react-bits JS component (ships without type declarations)
import CardSwap, { Card } from "@/components/CardSwap";
const SOURCES = [
{
name: "Fastmarkets",
tag: "فلزات و فولاد",
items: ["فلزات پایه", "سنگ‌ها و آلیاژها", "فولاد و گزارش قیمت", "ضایعات و مواد اولیه فولاد"],
},
{
name: "FerroAlloyNet",
tag: "فروآلیاژ",
items: [
"آلومینیوم، کروم، فروسیلیس",
"سنگ‌آهن، منگنز، منیزیم",
"مولیبدن، نیکل، تیتانیوم",
"تنگستن، وانادیوم، زیرکن",
],
},
{
name: "Platts (S&P)",
tag: "قیمت روزانه",
items: ["Metals Daily — قیمت روزانه‌ی فلزات"],
},
{
name: "Delphica",
tag: "فولاد CIS",
items: ["قیمت‌های هفتگی فولاد CIS", "قیمت‌های فولاد"],
},
{
name: "Baiinfo",
tag: "بازار چین",
items: ["گزارش بازار فولاد چین"],
},
];
export function ReportSources() {
return (
<section className="relative overflow-hidden bg-[#0b1018] py-16 text-white sm:py-24">
<div className="relative mx-auto max-w-6xl px-5 sm:min-h-[460px]">
{/* Text — stacked on mobile, left-aligned & vertically centered on sm+ */}
<div className="z-10 flex max-w-md flex-col text-right sm:absolute sm:inset-y-0 sm:left-0 sm:justify-center sm:px-2">
<span className="mb-4 inline-block w-fit rounded-full border border-white/15 bg-white/5 px-3 py-1 text-xs font-mono tracking-wider text-amber-300">
منابع داده
</span>
<h2 className="text-3xl font-bold leading-tight sm:text-4xl">
گزارش از معتبرترین منابع جهانی
</h2>
<p className="mt-4 leading-8 text-white/70">
دادهها و گزارشهای ما از منابع تخصصیِ بازار فلزات و فولاد جمعآوری میشوند از
Fastmarkets و Platts تا FerroAlloyNet، Delphica و Baiinfo.
</p>
<div className="mt-6 flex flex-wrap gap-2">
{SOURCES.map((s) => (
<span
key={s.name}
className="rounded-full border border-white/15 bg-white/5 px-3 py-1 text-xs text-white/80"
>
{s.name}
</span>
))}
</div>
</div>
{/* Card stack — own box on mobile (below text); peeks bottom-right on sm+ */}
<div className="relative mt-10 h-[300px] w-full overflow-hidden sm:static sm:mt-0 sm:h-auto sm:w-auto sm:overflow-visible">
<CardSwap
width={320}
height={230}
cardDistance={30}
verticalDistance={40}
delay={3500}
pauseOnHover={false}
skewAmount={9}
easing="linear"
>
{SOURCES.map((s) => (
<Card key={s.name} className="border-white/20 p-6">
<div className="flex items-center gap-2 text-amber-300">
<FileText className="h-5 w-5" />
<span className="font-mono text-xs tracking-wider">{s.tag}</span>
</div>
<h3 className="mt-3 text-2xl font-bold text-white">{s.name}</h3>
<ul className="mt-3 space-y-1.5 text-sm leading-6 text-white/70">
{s.items.map((it) => (
<li key={it}> {it}</li>
))}
</ul>
</Card>
))}
</CardSwap>
</div>
</div>
</section>
);
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,164 @@
import { ArrowLeft, ArrowRight } from "lucide-react";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Carousel,
type CarouselApi,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
type ReportItem = {
id: string;
title: string;
description: string;
href: string;
image: string;
};
const REPORTS: ReportItem[] = [
{
id: "domestic",
title: "بازار داخلی — ارز، طلا و سکه",
description: "نرخ زنده‌ی ارز، طلا و سکه؛ هر ۶۰ ثانیه با فلش رنگیِ تغییر.",
href: "/dashboard",
image:
"https://images.unsplash.com/photo-1610375461246-83df859d849d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&q=80&w=1080",
},
{
id: "commodity",
title: "کامودیتی جهانی",
description: "قیمت لحظه‌ای نفت، طلا، فلزات و محصولات کشاورزی از بازارهای جهانی.",
href: "/dashboard",
image:
"https://images.unsplash.com/photo-1582486225644-4a86c01a4f1c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&q=80&w=1080",
},
{
id: "metals",
title: "فلزات و فولاد",
description: "۶۷۴ محصولِ فلزی و فولادی با نمودار تاریخی و مقایسه‌ی تا ۵ مورد.",
href: "/dashboard",
image:
"https://images.unsplash.com/photo-1535813547-99c456a41d4a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&q=80&w=1080",
},
{
id: "world",
title: "اقتصاد جهانی",
description: "شاخص‌های کلانِ اقتصادهای بزرگ به‌صورت نقشه‌ی حرارتی.",
href: "/dashboard",
image:
"https://images.unsplash.com/photo-1526304640581-d334cdbbf45e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&q=80&w=1080",
},
{
id: "steel",
title: "سهام فولادی",
description: "بزرگ‌ترین شرکت‌های فولادی جهان بر اساس ارزش بازار، هر ۵ دقیقه.",
href: "/dashboard",
image:
"https://images.unsplash.com/photo-1565008447742-97f6f38c985c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&q=80&w=1080",
},
];
export function ReportsGallery() {
const [carouselApi, setCarouselApi] = useState<CarouselApi>();
const [canScrollPrev, setCanScrollPrev] = useState(false);
const [canScrollNext, setCanScrollNext] = useState(false);
const [currentSlide, setCurrentSlide] = useState(0);
useEffect(() => {
if (!carouselApi) return;
const update = () => {
setCanScrollPrev(carouselApi.canScrollPrev());
setCanScrollNext(carouselApi.canScrollNext());
setCurrentSlide(carouselApi.selectedScrollSnap());
};
update();
carouselApi.on("select", update);
return () => {
carouselApi.off("select", update);
};
}, [carouselApi]);
return (
<section className="py-20 sm:py-28">
<div className="mx-auto max-w-6xl px-5">
<div className="mb-8 flex items-end justify-between md:mb-12">
<div className="flex flex-col gap-3">
<h2 className="text-3xl font-bold md:text-4xl">گزارشها و بازارها</h2>
<p className="max-w-xl text-muted-foreground leading-7">
هر بخش از سامانهی دیدوان را اینجا ببینید؛ از بازار داخلی و کامودیتی تا فلزات، اقتصاد
جهانی و سهام فولادی.
</p>
</div>
<div className="hidden shrink-0 gap-2 md:flex">
<Button
size="icon"
variant="outline"
onClick={() => carouselApi?.scrollPrev()}
disabled={!canScrollPrev}
className="disabled:opacity-40"
aria-label="قبلی"
>
<ArrowRight className="size-5" />
</Button>
<Button
size="icon"
variant="outline"
onClick={() => carouselApi?.scrollNext()}
disabled={!canScrollNext}
className="disabled:opacity-40"
aria-label="بعدی"
>
<ArrowLeft className="size-5" />
</Button>
</div>
</div>
</div>
<div className="w-full">
<Carousel setApi={setCarouselApi} opts={{ align: "start", direction: "rtl" }}>
<CarouselContent className="mr-0 ml-0 px-5 sm:px-[max(1.25rem,calc(50vw-700px))]">
{REPORTS.map((item) => (
<CarouselItem key={item.id} className="basis-[300px] pr-5 lg:basis-[360px]">
<a href={item.href} className="group block rounded-xl">
<div className="relative h-full min-h-[26rem] overflow-hidden rounded-xl">
<img
src={item.image}
alt={item.title}
loading="lazy"
className="absolute inset-0 h-full w-full object-cover object-center transition-transform duration-300 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/85 via-black/35 to-transparent" />
<div className="absolute inset-x-0 bottom-0 flex flex-col items-start p-6 text-white">
<h3 className="mb-2 text-xl font-bold">{item.title}</h3>
<p className="mb-6 line-clamp-2 text-sm text-white/80 leading-6">
{item.description}
</p>
<span className="flex items-center gap-2 text-sm font-medium text-amber-300">
مشاهده
<ArrowLeft className="size-4 transition-transform group-hover:-translate-x-1" />
</span>
</div>
</div>
</a>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
<div className="mt-8 flex justify-center gap-2">
{REPORTS.map((_, index) => (
<button
key={index}
onClick={() => carouselApi?.scrollTo(index)}
aria-label={`اسلاید ${index + 1}`}
className={`h-2 w-2 rounded-full transition-colors ${
currentSlide === index ? "bg-primary" : "bg-primary/25"
}`}
/>
))}
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,188 @@
import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
import { motion } from "framer-motion";
import { Link, useLocation } from "@tanstack/react-router";
import { type LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
export interface NavItem {
name: string;
url?: string; // route mode (landing). Omit for controlled/state mode (dashboard)
icon: LucideIcon;
}
interface NavBarProps {
items: NavItem[];
className?: string;
/** false → render inline (no fixed positioning), e.g. inside a header */
floating?: boolean;
/** controlled active name; when set the component is driven by the parent */
active?: string;
onSelect?: (name: string) => void;
/** "lg" → roomier pill + larger icons (used for the mobile bottom bar) */
size?: "default" | "lg";
}
type Pos = { left: number; width: number; height: number };
export function TubelightNavbar({
items,
className,
floating = true,
active,
onSelect,
size = "default",
}: NavBarProps) {
const lg = size === "lg";
const scrollRef = useRef<HTMLDivElement>(null);
const innerRef = useRef<HTMLDivElement>(null);
const btnRefs = useRef<Record<string, HTMLElement | null>>({});
const location = useLocation();
const routeActive = items.find((i) => i.url === location.pathname)?.name ?? items[0].name;
const [internal, setInternal] = useState(active ?? routeActive);
const activeTab = active ?? internal;
useEffect(() => {
if (active === undefined) setInternal(routeActive);
}, [active, routeActive]);
// ── Edge-fade "swipe me" hint when the bar overflows ──
const [scrollable, setScrollable] = useState(false);
useEffect(() => {
const el = scrollRef.current;
if (!el) return;
const check = () => setScrollable(el.scrollWidth > el.clientWidth + 4);
check();
const ro = new ResizeObserver(check);
ro.observe(el);
return () => ro.disconnect();
}, [items.length]);
const fadeMask: CSSProperties | undefined = scrollable
? {
maskImage:
"linear-gradient(to right, transparent 0, #000 18px, #000 calc(100% - 18px), transparent 100%)",
WebkitMaskImage:
"linear-gradient(to right, transparent 0, #000 18px, #000 calc(100% - 18px), transparent 100%)",
}
: undefined;
// ── Measure the active tab so the glass pill can slide to it ──
const [pos, setPos] = useState<Pos | null>(null);
const prevLeft = useRef(0);
useEffect(() => {
const measure = () => {
const el = btnRefs.current[activeTab];
if (el) setPos({ left: el.offsetLeft, width: el.offsetWidth, height: el.offsetHeight });
};
measure();
const el = innerRef.current;
if (!el) return;
const ro = new ResizeObserver(measure);
ro.observe(el);
return () => ro.disconnect();
}, [activeTab, items.length, lg]);
// Squash toward the travel direction, like a drop of water settling.
const movingRight = pos ? pos.left >= prevLeft.current : true;
if (pos) prevLeft.current = pos.left;
const glide = { type: "spring" as const, stiffness: 320, damping: 30, mass: 0.9 };
const renderInner = (item: NavItem): ReactNode => {
const Icon = item.icon;
return (
<>
<span className="hidden md:inline">{item.name}</span>
<span className="md:hidden">
<Icon size={lg ? 24 : 18} strokeWidth={2.5} />
</span>
</>
);
};
const itemClass = (isActive: boolean) =>
cn(
"relative z-10 cursor-pointer rounded-full font-semibold whitespace-nowrap transition-[color,transform] duration-200",
lg ? "px-5 py-3 text-base" : "px-5 py-2 text-sm",
isActive ? "text-foreground" : "text-foreground/70 hover:text-primary hover:scale-[1.06]",
);
return (
<div
className={cn(
floating
? "fixed bottom-0 sm:top-0 left-1/2 -translate-x-1/2 z-50 mb-6 sm:pt-6"
: "inline-flex",
className,
)}
>
<div
ref={scrollRef}
style={fadeMask}
className={cn(
"lg-bar flex max-w-[92vw] items-center overflow-x-auto rounded-full",
lg ? "px-1.5 py-1.5" : "px-1 py-1",
)}
>
<div
ref={innerRef}
className={cn("relative flex w-max items-center", lg ? "gap-2" : "gap-1.5")}
>
{pos && (
<motion.span
aria-hidden
className="lg-pill pointer-events-none absolute left-0 top-1/2 z-0 block rounded-full"
initial={false}
style={{ transformOrigin: movingRight ? "left center" : "right center" }}
animate={{
x: pos.left,
y: -pos.height / 2,
width: pos.width,
height: pos.height,
scaleX: [1, 1.14, 1],
}}
transition={{
x: glide,
y: glide,
width: glide,
height: glide,
scaleX: { duration: 0.44, ease: [1, 0, 0.4, 1], times: [0, 0.5, 1] },
}}
/>
)}
{items.map((item) => {
const isActive = activeTab === item.name;
const handle = () => {
setInternal(item.name);
onSelect?.(item.name);
};
const ref = (el: HTMLElement | null) => {
btnRefs.current[item.name] = el;
};
return item.url ? (
<Link
key={item.name}
to={item.url}
ref={ref as never}
onClick={handle}
className={itemClass(isActive)}
>
{renderInner(item)}
</Link>
) : (
<button
key={item.name}
type="button"
ref={ref as never}
onClick={handle}
className={itemClass(isActive)}
>
{renderInner(item)}
</button>
);
})}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,51 @@
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-sm font-medium cursor-pointer transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@ -0,0 +1,115 @@
import * as React from "react";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:rounded-lg",
className,
)}
{...props}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
);
AlertDialogHeader.displayName = "AlertDialogHeader";
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props}
/>
);
AlertDialogFooter.displayName = "AlertDialogFooter";
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
{...props}
/>
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};

View File

@ -0,0 +1,49 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
),
);
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
));
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription };

View File

@ -0,0 +1,5 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
const AspectRatio = AspectRatioPrimitive.Root;
export { AspectRatio };

View File

@ -0,0 +1,47 @@
"use client";
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils";
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className,
)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback };

View File

@ -0,0 +1,32 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants };

View File

@ -0,0 +1,101 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils";
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className,
)}
{...props}
/>
),
);
BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
({ className, ...props }, ref) => (
<li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
),
);
BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a";
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
);
});
BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
),
);
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};

View File

@ -0,0 +1,49 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };

View File

@ -0,0 +1,177 @@
"use client";
import * as React from "react";
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
import { cn } from "@/lib/utils";
import { Button, buttonVariants } from "@/components/ui/button";
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"];
}) {
const defaultClassNames = getDefaultClassNames();
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className,
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) => date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn("relative flex flex-col gap-4 md:flex-row", defaultClassNames.months),
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
nav: cn(
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
defaultClassNames.nav,
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"h-(--cell-size) w-(--cell-size) select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_previous,
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"h-(--cell-size) w-(--cell-size) select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_next,
),
month_caption: cn(
"flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
defaultClassNames.month_caption,
),
dropdowns: cn(
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
defaultClassNames.dropdowns,
),
dropdown_root: cn(
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
defaultClassNames.dropdown_root,
),
dropdown: cn("bg-popover absolute inset-0 opacity-0", defaultClassNames.dropdown),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
defaultClassNames.caption_label,
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
defaultClassNames.weekday,
),
week: cn("mt-2 flex w-full", defaultClassNames.week),
week_number_header: cn("w-(--cell-size) select-none", defaultClassNames.week_number_header),
week_number: cn(
"text-muted-foreground select-none text-[0.8rem]",
defaultClassNames.week_number,
),
day: cn(
"group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
defaultClassNames.day,
),
range_start: cn("bg-accent rounded-l-md", defaultClassNames.range_start),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today,
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside,
),
disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return <div data-slot="calendar" ref={rootRef} className={cn(className)} {...props} />;
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return <ChevronLeftIcon className={cn("size-4", className)} {...props} />;
}
if (orientation === "right") {
return <ChevronRightIcon className={cn("size-4", className)} {...props} />;
}
return <ChevronDownIcon className={cn("size-4", className)} {...props} />;
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
);
},
...components,
}}
{...props}
/>
);
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames();
const ref = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus();
}, [modifiers.focused]);
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-(--cell-size) flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className,
)}
{...props}
/>
);
}
export { Calendar, CalendarDayButton };

View File

@ -0,0 +1,55 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
{...props}
/>
),
);
Card.displayName = "Card";
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
),
);
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
),
);
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
),
);
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
),
);
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
),
);
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };

View File

@ -0,0 +1,240 @@
import * as React from "react";
import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return context;
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }, ref) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins,
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return;
}
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext],
);
React.useEffect(() => {
if (!api || !setApi) {
return;
}
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
if (!api) {
return;
}
onSelect(api);
api.on("reInit", onSelect);
api.on("select", onSelect);
return () => {
api?.off("select", onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
});
Carousel.displayName = "Carousel";
const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel();
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className,
)}
{...props}
/>
</div>
);
},
);
CarouselContent.displayName = "CarouselContent";
const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const { orientation } = useCarousel();
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className,
)}
{...props}
/>
);
},
);
CarouselItem.displayName = "CarouselItem";
const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
);
},
);
CarouselPrevious.displayName = "CarouselPrevious";
const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
);
},
);
CarouselNext.displayName = "CarouselNext";
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
};

View File

@ -0,0 +1,331 @@
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "@/lib/utils";
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
);
};
type ChartContextProps = {
config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
}
return context;
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig;
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className,
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
});
ChartContainer.displayName = "Chart";
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color);
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`,
)
.join("\n"),
}}
/>
);
};
const ChartTooltip = RechartsPrimitive.Tooltip;
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref,
) => {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
}
const [item] = payload;
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>
);
}
if (!value) {
return null;
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);
if (!active || !payload?.length) {
return null;
}
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className,
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload
.filter((item) => item.type !== "none")
.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center",
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
},
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center",
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
},
);
ChartTooltipContent.displayName = "ChartTooltip";
const ChartLegend = RechartsPrimitive.Legend;
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean;
nameKey?: string;
}
>(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
const { config } = useChart();
if (!payload?.length) {
return null;
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className,
)}
>
{payload
.filter((item) => item.type !== "none")
.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
});
ChartLegendContent.displayName = "ChartLegend";
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
if (typeof payload !== "object" || payload === null) {
return undefined;
}
const payloadPayload =
"payload" in payload && typeof payload.payload === "object" && payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
}
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
};

View File

@ -0,0 +1,26 @@
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator className={cn("grid place-content-center text-current")}>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };

View File

@ -0,0 +1,11 @@
"use client";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View File

@ -0,0 +1,143 @@
"use client";
import * as React from "react";
import { type DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import { cn } from "@/lib/utils";
import { Dialog, DialogContent } from "@/components/ui/dialog";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className,
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
const CommandDialog = ({ children, ...props }: DialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className,
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className,
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
{...props}
/>
);
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};

View File

@ -0,0 +1,187 @@
import * as React from "react";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const ContextMenu = ContextMenuPrimitive.Root;
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
const ContextMenuGroup = ContextMenuPrimitive.Group;
const ContextMenuPortal = ContextMenuPrimitive.Portal;
const ContextMenuSub = ContextMenuPrimitive.Sub;
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
));
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-context-menu-content-transform-origin)",
className,
)}
{...props}
/>
));
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-context-menu-content-transform-origin)",
className,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
));
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className,
)}
{...props}
/>
));
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
));
ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-4 w-4 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
));
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold text-foreground", inset && "pl-8", className)}
{...props}
/>
));
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
));
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
{...props}
/>
);
};
ContextMenuShortcut.displayName = "ContextMenuShortcut";
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
};

View File

@ -0,0 +1,104 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:rounded-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background cursor-pointer transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};

View File

@ -0,0 +1,98 @@
import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/lib/utils";
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
);
Drawer.displayName = "Drawer";
const DrawerTrigger = DrawerPrimitive.Trigger;
const DrawerPortal = DrawerPrimitive.Portal;
const DrawerClose = DrawerPrimitive.Close;
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
));
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className,
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
));
DrawerContent.displayName = "DrawerContent";
const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} {...props} />
);
DrawerHeader.displayName = "DrawerHeader";
const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
);
DrawerFooter.displayName = "DrawerFooter";
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
};

View File

@ -0,0 +1,188 @@
"use client";
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin)",
className,
)}
{...props}
/>
));
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin)",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};

View File

@ -0,0 +1,171 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import {
Controller,
FormProvider,
useFormContext,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form";
import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label";
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue | null>(null);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
if (!itemContext) {
throw new Error("useFormField should be used within <FormItem>");
}
const fieldState = getFieldState(fieldContext.name, formState);
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue | null>(null);
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
},
);
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
);
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
);
});
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props}
/>
);
});
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message ?? "") : children;
if (!body) {
return null;
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-[0.8rem] font-medium text-destructive", className)}
{...props}
>
{body}
</p>
);
});
FormMessage.displayName = "FormMessage";
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
};

View File

@ -0,0 +1,27 @@
import * as React from "react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import { cn } from "@/lib/utils";
const HoverCard = HoverCardPrimitive.Root;
const HoverCardTrigger = HoverCardPrimitive.Trigger;
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-hover-card-content-transform-origin)",
className,
)}
{...props}
/>
));
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
export { HoverCard, HoverCardTrigger, HoverCardContent };

View File

@ -0,0 +1,69 @@
import * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp";
import { Minus } from "lucide-react";
import { cn } from "@/lib/utils";
const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>,
React.ComponentPropsWithoutRef<typeof OTPInput>
>(({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn(
"flex items-center gap-2 has-[:disabled]:opacity-50",
containerClassName,
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
));
InputOTP.displayName = "InputOTP";
const InputOTPGroup = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center", className)} {...props} />
));
InputOTPGroup.displayName = "InputOTPGroup";
const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> & { index: number }
>(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
return (
<div
ref={ref}
className={cn(
"relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-1 ring-ring",
className,
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
</div>
)}
</div>
);
});
InputOTPSlot.displayName = "InputOTPSlot";
const InputOTPSeparator = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<Minus />
</div>
));
InputOTPSeparator.displayName = "InputOTPSeparator";
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

View File

@ -0,0 +1,22 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input };

View File

@ -0,0 +1,21 @@
"use client";
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };

View File

@ -0,0 +1,229 @@
import * as React from "react";
import * as MenubarPrimitive from "@radix-ui/react-menubar";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
function MenubarMenu({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu {...props} />;
}
function MenubarGroup({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group {...props} />;
}
function MenubarPortal({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal {...props} />;
}
function MenubarRadioGroup({ ...props }: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return <MenubarPrimitive.RadioGroup {...props} />;
}
function MenubarSub({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
}
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn(
"flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm",
className,
)}
{...props}
/>
));
Menubar.displayName = MenubarPrimitive.Root.displayName;
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
className,
)}
{...props}
/>
));
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
));
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-menubar-content-transform-origin)",
className,
)}
{...props}
/>
));
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(({ className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, ref) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-menubar-content-transform-origin)",
className,
)}
{...props}
/>
</MenubarPrimitive.Portal>
));
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className,
)}
{...props}
/>
));
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
));
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="h-4 w-4 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
));
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props}
/>
));
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
{...props}
/>
);
};
MenubarShortcut.displayname = "MenubarShortcut";
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
};

View File

@ -0,0 +1,120 @@
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn("relative z-10 flex max-w-max flex-1 items-center justify-center", className)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn("group flex flex-1 list-none items-center justify-center space-x-1", className)}
{...props}
/>
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium cursor-pointer transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent",
);
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className,
)}
{...props}
/>
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className,
)}
ref={ref}
{...props}
/>
</div>
));
NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className,
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
};

View File

@ -0,0 +1,98 @@
import * as React from "react";
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils";
import { ButtonProps, buttonVariants } from "@/components/ui/button";
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
);
Pagination.displayName = "Pagination";
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
({ className, ...props }, ref) => (
<ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} />
),
);
PaginationContent.displayName = "PaginationContent";
const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(
({ className, ...props }, ref) => <li ref={ref} className={cn("", className)} {...props} />,
);
PaginationItem.displayName = "PaginationItem";
type PaginationLinkProps = {
isActive?: boolean;
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">;
const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className,
)}
{...props}
/>
);
PaginationLink.displayName = "PaginationLink";
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
);
PaginationPrevious.displayName = "PaginationPrevious";
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
);
PaginationNext.displayName = "PaginationNext";
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
);
PaginationEllipsis.displayName = "PaginationEllipsis";
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
};

View File

@ -0,0 +1,31 @@
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-popover-content-transform-origin)",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

View File

@ -0,0 +1,25 @@
"use client";
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@/lib/utils";
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn("relative h-2 w-full overflow-hidden rounded-full bg-primary/20", className)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress };

View File

@ -0,0 +1,36 @@
import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />;
});
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow cursor-pointer focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-3.5 w-3.5 fill-primary" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
});
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
export { RadioGroup, RadioGroupItem };

View File

@ -0,0 +1,37 @@
import { GripVertical } from "lucide-react";
import { Group, Panel, Separator } from "react-resizable-panels";
import { cn } from "@/lib/utils";
const ResizablePanelGroup = ({ className, ...props }: React.ComponentProps<typeof Group>) => (
<Group
className={cn("flex h-full w-full data-[panel-group-direction=vertical]:flex-col", className)}
{...props}
/>
);
const ResizablePanel = Panel;
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof Separator> & {
withHandle?: boolean;
}) => (
<Separator
className={cn(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className,
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</Separator>
);
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };

View File

@ -0,0 +1,44 @@
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils";
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };

Some files were not shown because too many files have changed in this diff Show More