238 lines
7.3 KiB
Markdown
238 lines
7.3 KiB
Markdown
# marketplace-web
|
|
|
|
Web app for listing compute **offers**, placing **bids**, and viewing **market stats** in the AITBC stack.
|
|
Stage-aware: works **now** against a mock API, later switches to **coordinator/pool-hub/blockchain** endpoints without touching the UI.
|
|
|
|
## Goals
|
|
|
|
1. Browse offers (GPU/CPU, price per token, location, queue, latency).
|
|
2. Place/manage bids (instant or scheduled).
|
|
3. Watch market stats (price trends, filled volume, miner capacity).
|
|
4. Wallet view (balance, recent tx; read-only first).
|
|
5. Internationalization (EU langs later), dark theme, 960px layout.
|
|
|
|
## Tech/Structure (Windsurf-friendly)
|
|
|
|
- Vanilla **TypeScript + Vite** (no React), separate JS/CSS files.
|
|
- File layout (desktop 960px grid, mobile-first CSS, dark theme):
|
|
|
|
```
|
|
marketplace-web/
|
|
├─ public/
|
|
│ ├─ icons/ # favicons, app icons
|
|
│ └─ i18n/ # JSON dictionaries (en/… later)
|
|
├─ src/
|
|
│ ├─ app.ts # app bootstrap/router
|
|
│ ├─ router.ts # hash-router (/, /offer/:id, /bids, /stats, /wallet)
|
|
│ ├─ api/
|
|
│ │ ├─ http.ts # fetch wrapper + baseURL swap (mock → real)
|
|
│ │ └─ marketplace.ts # typed API calls
|
|
│ ├─ store/
|
|
│ │ ├─ state.ts # global app state (signals or tiny pubsub)
|
|
│ │ └─ types.ts # shared types/interfaces
|
|
│ ├─ views/
|
|
│ │ ├─ HomeView.ts
|
|
│ │ ├─ OfferDetailView.ts
|
|
│ │ ├─ BidsView.ts
|
|
│ │ ├─ StatsView.ts
|
|
│ │ └─ WalletView.ts
|
|
│ ├─ components/
|
|
│ │ ├─ OfferCard.ts
|
|
│ │ ├─ BidForm.ts
|
|
│ │ ├─ Table.ts
|
|
│ │ ├─ Sparkline.ts # minimal chart (no external lib)
|
|
│ │ └─ Toast.ts
|
|
│ ├─ styles/
|
|
│ │ ├─ base.css # reset, variables, dark theme
|
|
│ │ ├─ layout.css # 960px grid, sections, header/footer
|
|
│ │ └─ components.css
|
|
│ └─ util/
|
|
│ ├─ format.ts # fmt token, price, time
|
|
│ ├─ validate.ts # input validation
|
|
│ └─ i18n.ts # simple t() loader
|
|
├─ index.html
|
|
├─ vite.config.ts
|
|
└─ README.md
|
|
```
|
|
|
|
## Routes (Hash router)
|
|
|
|
- `/` — Offer list + filters
|
|
- `/offer/:id` — Offer details + **BidForm**
|
|
- `/bids` — User bids (open, filled, cancelled)
|
|
- `/stats` — Price/volume/capacity charts
|
|
- `/wallet` — Balance + last 10 tx (read-only)
|
|
|
|
## UI/UX Spec
|
|
|
|
- **Dark theme**, accent = ice-blue/white outlines (fits OIB style).
|
|
- **960px max width** desktop, mobile-first, Nothing Phone 2a as reference.
|
|
- **Toast** bottom-center for actions.
|
|
- Forms: no animations, clear validation, disable buttons during submit.
|
|
|
|
## Data Types (minimal)
|
|
|
|
```ts
|
|
type TokenAmount = `${number}`; // keep as string to avoid FP errors
|
|
type PricePerToken = `${number}`;
|
|
|
|
interface Offer {
|
|
id: string;
|
|
provider: string; // miner or pool label
|
|
hw: { gpu: string; vramGB?: number; cpu?: string };
|
|
region: string; // e.g., eu-central
|
|
queue: number; // jobs waiting
|
|
latencyMs: number;
|
|
price: PricePerToken; // AIToken per 1k tokens processed
|
|
minTokens: number;
|
|
maxTokens: number;
|
|
updatedAt: string;
|
|
}
|
|
|
|
interface BidInput {
|
|
offerId: string;
|
|
tokens: number; // requested tokens to process
|
|
maxPrice: PricePerToken; // cap
|
|
}
|
|
|
|
interface Bid extends BidInput {
|
|
id: string;
|
|
status: "open" | "filled" | "cancelled" | "expired";
|
|
createdAt: string;
|
|
filledTokens?: number;
|
|
avgFillPrice?: PricePerToken;
|
|
}
|
|
|
|
interface MarketStats {
|
|
ts: string[];
|
|
medianPrice: number[]; // per interval
|
|
filledVolume: number[]; // tokens
|
|
capacity: number[]; // available tokens
|
|
}
|
|
|
|
interface Wallet {
|
|
address: string;
|
|
balance: TokenAmount;
|
|
recent: Array<{ id: string; kind: "mint"|"spend"|"refund"; amount: TokenAmount; at: string }>;
|
|
}
|
|
```
|
|
|
|
## Mock API (Stage 0)
|
|
|
|
Base URL: `/.mock` (served via Vite dev middleware or static JSON)
|
|
|
|
- `GET /.mock/offers.json` → `Offer[]`
|
|
- `GET /.mock/offers/:id.json` → `Offer`
|
|
- `POST /.mock/bids` (body `BidInput`) → `Bid`
|
|
- `GET /.mock/bids.json` → `Bid[]`
|
|
- `GET /.mock/stats.json` → `MarketStats`
|
|
- `GET /.mock/wallet.json` → `Wallet`
|
|
|
|
Switch to real endpoints by changing **`BASE_URL`** in `api/http.ts`.
|
|
|
|
## Real API (Stage 2/3/4 wiring)
|
|
|
|
When coordinator/pool-hub/blockchain are ready:
|
|
|
|
- `GET /api/market/offers` → `Offer[]`
|
|
- `GET /api/market/offers/:id` → `Offer`
|
|
- `POST /api/market/bids` → create bid, returns `Bid`
|
|
- `GET /api/market/bids?owner=<wallet>` → `Bid[]`
|
|
- `GET /api/market/stats?range=24h|7d|30d` → `MarketStats`
|
|
- `GET /api/wallet/summary?addr=<wallet>` → `Wallet`
|
|
|
|
Auth header (later): `Authorization: Bearer <session-or-wallet-token>`.
|
|
|
|
## State & Caching
|
|
|
|
- In-memory store (`store/state.ts`) with tiny pub/sub.
|
|
- Offer list cached 30s; stats cached 60s; bust on route change if stale.
|
|
- Optimistic UI for **Bid** create; reconcile on server response.
|
|
|
|
## Filters (Home)
|
|
|
|
- Region (multi)
|
|
- HW capability (GPU model, min VRAM, CPU present)
|
|
- Price range (slider)
|
|
- Latency max (ms)
|
|
- Queue max
|
|
|
|
All filters are client-side over fetched offers (server-side later).
|
|
|
|
## Validation Rules
|
|
|
|
- `BidInput.tokens` ∈ `[offer.minTokens, offer.maxTokens]`.
|
|
- `maxPrice >= offer.price` for instant fill hint; otherwise place as limit.
|
|
- Warn if `queue > threshold` or `latencyMs > threshold`.
|
|
|
|
## Security Notes (Web)
|
|
|
|
- Input sanitize; never eval.
|
|
- CSRF not needed for read-only; for POST use standard token once auth exists.
|
|
- Rate-limit POST (server).
|
|
- Display wallet **read-only** unless signing is integrated (later via wallet-daemon).
|
|
|
|
## i18n
|
|
|
|
- `public/i18n/en.json` as root.
|
|
- `util/i18n.ts` provides `t(key, params?)`.
|
|
- Keys only (no concatenated sentences). EU languages can be added later via your i18n tool.
|
|
|
|
## Accessibility
|
|
|
|
- Semantic HTML, label every input, focus states visible.
|
|
- Keyboard: Tab order, Enter submits forms, Esc closes dialogs.
|
|
- Color contrast AA in dark theme.
|
|
|
|
## Minimal Styling Rules
|
|
|
|
- `styles/base.css`: CSS variables for colors, spacing, radius.
|
|
- `styles/layout.css`: header, main container (max-width: 960px), grid for cards.
|
|
- `styles/components.css`: OfferCard, Table, Buttons, Toast.
|
|
|
|
## Testing
|
|
|
|
- Manual first:
|
|
- Offers list loads, filters act.
|
|
- Place bid with edge values.
|
|
- Stats sparkline renders with missing points.
|
|
- Later: Vitest for `util/` + `api/` modules.
|
|
|
|
## Env/Config
|
|
|
|
- `VITE_API_BASE` → mock or real.
|
|
- `VITE_DEFAULT_REGION` → optional default filter.
|
|
- `VITE_FEATURE_WALLET=readonly|disabled`.
|
|
|
|
## Build/Run
|
|
|
|
```
|
|
# dev
|
|
npm i
|
|
npm run dev
|
|
|
|
# build
|
|
npm run build
|
|
npm run preview
|
|
```
|
|
|
|
## Migration Checklist (Mock → Real)
|
|
|
|
1. Replace `VITE_API_BASE` with coordinator gateway URL.
|
|
2. Enable auth header injection when session is present.
|
|
3. Wire `/wallet` to wallet-daemon read endpoint.
|
|
4. Swap stats source to real telemetry.
|
|
5. Keep the same types; server must honor them.
|
|
|
|
## Open Tasks
|
|
|
|
- [ ] Create file skeletons per structure above.
|
|
- [ ] Add mock JSON under `public/.mock/`.
|
|
- [ ] Implement OfferCard + filters.
|
|
- [ ] Implement BidForm with validation + optimistic UI.
|
|
- [ ] Implement StatsView with `Sparkline` (no external chart lib).
|
|
- [ ] Wire `VITE_API_BASE` switch.
|
|
- [ ] Basic a11y pass + dark theme polish.
|
|
- [ ] Wallet view (read-only).
|
|
|