Based on the repository's commit message style and the changes in the diff, here's an appropriate commit message:
``` feat: add websocket tests, PoA metrics, marketplace endpoints, and enhanced observability - Add comprehensive websocket tests for blocks and transactions streams including multi-subscriber and high-volume scenarios - Extend PoA consensus with per-proposer block metrics and rotation tracking - Add latest block interval gauge and RPC error spike alerting - Enhance mock coordinator
This commit is contained in:
@ -6,7 +6,6 @@ Static web explorer for the AITBC blockchain node, displaying blocks, transactio
|
||||
|
||||
## Development Setup
|
||||
|
||||
- Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
@ -14,145 +13,33 @@ Static web explorer for the AITBC blockchain node, displaying blocks, transactio
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
- The explorer ships with mock data in `public/mock/` that powers the tables by default.
|
||||
The dev server listens on `http://localhost:5173/` by default. Adjust via `--host`/`--port` flags in the `systemd` unit or `package.json` script.
|
||||
|
||||
### Data Mode Toggle
|
||||
## Data Mode Toggle
|
||||
|
||||
- Configuration lives in `src/config.ts` and can be overridden with environment variables.
|
||||
- Use `VITE_DATA_MODE` to choose between `mock` (default) and `live`.
|
||||
- When switching to live data, set `VITE_COORDINATOR_API` to the coordinator base URL (e.g. `http://localhost:8000`).
|
||||
- When switching to live data, set `VITE_COORDINATOR_API` to the coordinator base URL (e.g., `http://localhost:8000`).
|
||||
- Example `.env` snippet:
|
||||
```bash
|
||||
VITE_DATA_MODE=live
|
||||
VITE_COORDINATOR_API=https://coordinator.dev.internal
|
||||
```
|
||||
With live mode enabled, the SPA will request `/v1/<resource>` routes from the coordinator instead of the bundled mock JSON.
|
||||
|
||||
## Next Steps
|
||||
## Feature Flags & Auth
|
||||
|
||||
- Build out responsive styling and navigation interactions.
|
||||
- Extend the data layer to support coordinator authentication and pagination when live endpoints are ready.
|
||||
- Document coordinator API assumptions once the backend contracts stabilize.
|
||||
- Document any backend expectations (e.g., coordinator accepting bearer tokens) alongside the environment variables in deployment manifests.
|
||||
|
||||
## Coordinator API Contracts (Draft)
|
||||
## End-to-End Tests
|
||||
|
||||
- **Blocks** (`GET /v1/blocks?limit=&offset=`)
|
||||
- Expected payload:
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"height": 12045,
|
||||
"hash": "0x...",
|
||||
"timestamp": "2025-09-27T01:58:12Z",
|
||||
"tx_count": 8,
|
||||
"proposer": "miner-alpha"
|
||||
}
|
||||
],
|
||||
"next_offset": 12040
|
||||
}
|
||||
```
|
||||
- TODO: confirm pagination fields and proposer metadata.
|
||||
- Install browsers after `npm install` by running `npx playwright install`.
|
||||
- Launch the dev server (or point `EXPLORER_BASE_URL` at an already running instance) and run:
|
||||
```bash
|
||||
npm run test:e2e
|
||||
```
|
||||
- Tests automatically persist live mode and stub coordinator responses to verify overview, blocks, and transactions views.
|
||||
|
||||
- **Transactions** (`GET /v1/transactions?limit=&offset=`)
|
||||
- Expected payload:
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"hash": "0x...",
|
||||
"block": 12045,
|
||||
"from": "0x...",
|
||||
"to": "0x...",
|
||||
"value": "12.5",
|
||||
"status": "Succeeded"
|
||||
}
|
||||
],
|
||||
"next_offset": "0x..."
|
||||
}
|
||||
```
|
||||
- TODO: finalize value units (AIT vs wei) and status enum.
|
||||
## Playwright
|
||||
|
||||
- **Addresses** (`GET /v1/addresses/{address}`)
|
||||
- Expected payload:
|
||||
```json
|
||||
{
|
||||
"address": "0x...",
|
||||
"balance": "1450.25",
|
||||
"tx_count": 42,
|
||||
"last_active": "2025-09-27T01:48:00Z",
|
||||
"recent_transactions": ["0x..."]
|
||||
}
|
||||
```
|
||||
- TODO: detail pagination for recent transactions and add receipt summary references.
|
||||
|
||||
- **Receipts** (`GET /v1/jobs/{job_id}/receipts`)
|
||||
- Expected payload:
|
||||
```json
|
||||
{
|
||||
"job_id": "job-0001",
|
||||
"items": [
|
||||
{
|
||||
"receipt_id": "rcpt-123",
|
||||
"miner": "miner-alpha",
|
||||
"coordinator": "coordinator-001",
|
||||
"issued_at": "2025-09-27T01:52:22Z",
|
||||
"status": "Attested",
|
||||
"payload": {
|
||||
"miner_signature": "0x...",
|
||||
"coordinator_signature": "0x..."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
- TODO: confirm signature payload structure and include attestation metadata.
|
||||
|
||||
## Styling Guide
|
||||
|
||||
- **`public/css/base.css`**
|
||||
- Defines global typography, color scheme, and utility classes (tables, placeholders, code tags).
|
||||
- Use this file for cross-page primitives and reset/normalization rules.
|
||||
- When adding new utilities (e.g., badges, alerts), document them in this section and keep naming consistent with the existing BEM-lite approach.
|
||||
|
||||
- **`public/css/layout.css`**
|
||||
- Contains structural styles for the Explorer shell (header, footer, cards, forms, grids).
|
||||
- Encapsulate component-specific classes with a predictable prefix, such as `.blocks__table`, `.addresses__input-group`, or `.receipts__controls`.
|
||||
- Prefer utility classes from `base.css` when possible, and only introduce new layout classes when a component requires dedicated styling.
|
||||
|
||||
- **Adding New Components**
|
||||
- Create semantic markup first in `src/pages/` or `src/components/`, using descriptive class names that map to the page or component (`.transactions__filter`, `.overview__chart`).
|
||||
- Extend `layout.css` with matching selectors to style the new elements; keep related rules grouped together for readability.
|
||||
- For reusable widgets across multiple pages, consider extracting shared styles into a dedicated section or introducing a new partial CSS file when the component becomes complex.
|
||||
|
||||
## Deployment Notes
|
||||
|
||||
- **Environment Variables**
|
||||
- `VITE_DATA_MODE`: `mock` (default) or `live`.
|
||||
- `VITE_COORDINATOR_API`: Base URL for coordinator API when `live` mode is enabled.
|
||||
- Additional Vite variables can be added following the `VITE_*` naming convention.
|
||||
|
||||
- **Mock vs Live**
|
||||
- In non-production environments, keep `VITE_DATA_MODE=mock` to serve the static JSON under `public/mock/` for quick demos.
|
||||
- For staging/production deployments, set `VITE_DATA_MODE=live` and ensure the coordinator endpoint is reachable from the frontend origin; configure CORS accordingly on the backend.
|
||||
- Consider serving mock JSON from a CDN or static bucket if you want deterministic demos while backend dependencies are under development.
|
||||
|
||||
- **Build & Deploy**
|
||||
- Build command: `npm run build` (outputs to `dist/`).
|
||||
- Preview locally with `npm run preview` before publishing.
|
||||
- Deploy the `dist/` contents to your static host (e.g., Nginx, S3 + CloudFront, Vercel). Ensure environment variables are injected at build time or through runtime configuration mechanisms supported by your hosting provider.
|
||||
|
||||
## Error Handling (Live Mode)
|
||||
|
||||
- **Status Codes**
|
||||
- `2xx`: Treat as success; map response bodies into the typed models in `src/lib/models.ts`.
|
||||
- `4xx`: Surface actionable messages to the user (e.g., invalid job ID). For `404`, show “not found” states in the relevant page. For `429`, display a rate-limit notice and back off.
|
||||
- `5xx`: Show a generic coordinator outage message and trigger retry logic.
|
||||
|
||||
- **Retry Strategy**
|
||||
- Use an exponential backoff with jitter when retrying `5xx` or network failures (suggested base delay 500 ms, max 5 attempts).
|
||||
- Do not retry on `4xx` except `429`; instead, display feedback.
|
||||
|
||||
- **Telemetry & Logging**
|
||||
- Consider emitting console warnings or hooking into an analytics layer when retries occur, noting the endpoint and status code.
|
||||
- Bubble critical errors via a shared notification component so users understand whether data is stale or unavailable.
|
||||
- Run `npm run test:e2e` to execute the end-to-end tests.
|
||||
- The tests will automatically persist live mode and stub coordinator responses to verify overview, blocks, and transactions views.
|
||||
|
||||
@ -5,10 +5,13 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.48.0",
|
||||
"@types/node": "^20.12.7",
|
||||
"typescript": "^5.4.0",
|
||||
"vite": "^5.2.0"
|
||||
}
|
||||
|
||||
23
apps/explorer-web/playwright.config.ts
Normal file
23
apps/explorer-web/playwright.config.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const PORT = process.env.EXPLORER_DEV_PORT ?? "5173";
|
||||
const HOST = process.env.EXPLORER_DEV_HOST ?? "127.0.0.1";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e",
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
reporter: process.env.CI ? "github" : "list",
|
||||
use: {
|
||||
baseURL: process.env.EXPLORER_BASE_URL ?? `http://${HOST}:${PORT}`,
|
||||
trace: "on-first-retry",
|
||||
viewport: { width: 1280, height: 720 },
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -29,12 +29,52 @@
|
||||
flex: 1 1 45%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.addresses__input-group,
|
||||
.receipts__input-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.overview__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.table thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table tr {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid rgba(125, 196, 255, 0.12);
|
||||
}
|
||||
|
||||
.table td {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.overview__grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
}
|
||||
|
||||
.table thead {
|
||||
display: table-header-group;
|
||||
}
|
||||
|
||||
.table tr {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.table td {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.toast-container {
|
||||
left: 0;
|
||||
right: 0;
|
||||
@ -169,6 +209,18 @@
|
||||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.overview__grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
}
|
||||
|
||||
.table td {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header {
|
||||
|
||||
@ -4,6 +4,7 @@ import type {
|
||||
BlockListResponse,
|
||||
TransactionListResponse,
|
||||
AddressDetailResponse,
|
||||
AddressListResponse,
|
||||
ReceiptListResponse,
|
||||
BlockSummary,
|
||||
TransactionSummary,
|
||||
@ -11,7 +12,33 @@ import type {
|
||||
ReceiptSummary,
|
||||
} from "./models.ts";
|
||||
|
||||
let currentMode: DataMode = CONFIG.dataMode;
|
||||
const STORAGE_KEY = "aitbc-explorer:data-mode";
|
||||
|
||||
function loadStoredMode(): DataMode | null {
|
||||
if (typeof window === "undefined") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const value = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (value === "mock" || value === "live") {
|
||||
return value as DataMode;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[Explorer] Unable to read stored data mode", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const initialMode = loadStoredMode() ?? CONFIG.dataMode;
|
||||
let currentMode: DataMode = initialMode;
|
||||
|
||||
function syncDocumentMode(mode: DataMode): void {
|
||||
if (typeof document !== "undefined") {
|
||||
document.documentElement.dataset.mode = mode;
|
||||
}
|
||||
}
|
||||
|
||||
syncDocumentMode(currentMode);
|
||||
|
||||
export function getDataMode(): DataMode {
|
||||
return currentMode;
|
||||
@ -19,6 +46,14 @@ export function getDataMode(): DataMode {
|
||||
|
||||
export function setDataMode(mode: DataMode): void {
|
||||
currentMode = mode;
|
||||
syncDocumentMode(mode);
|
||||
if (typeof window !== "undefined") {
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, mode);
|
||||
} catch (error) {
|
||||
console.warn("[Explorer] Failed to persist data mode", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchBlocks(): Promise<BlockSummary[]> {
|
||||
@ -28,15 +63,15 @@ export async function fetchBlocks(): Promise<BlockSummary[]> {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${CONFIG.apiBaseUrl}/v1/blocks`);
|
||||
const response = await fetch(`${CONFIG.apiBaseUrl}/v1/explorer/blocks`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch blocks: ${response.status}`);
|
||||
throw new Error(`Failed to fetch blocks: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const data = (await response.json()) as BlockListResponse;
|
||||
return data.items;
|
||||
} catch (error) {
|
||||
console.warn("[Explorer] Failed to fetch live block data", error);
|
||||
notifyError("Unable to load live block data. Displaying placeholders.");
|
||||
console.error("[Explorer] Failed to fetch live block data", error);
|
||||
notifyError("Unable to load live block data from coordinator. Showing placeholders.");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -48,15 +83,15 @@ export async function fetchTransactions(): Promise<TransactionSummary[]> {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${CONFIG.apiBaseUrl}/v1/transactions`);
|
||||
const response = await fetch(`${CONFIG.apiBaseUrl}/v1/explorer/transactions`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch transactions: ${response.status}`);
|
||||
throw new Error(`Failed to fetch transactions: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const data = (await response.json()) as TransactionListResponse;
|
||||
return data.items;
|
||||
} catch (error) {
|
||||
console.warn("[Explorer] Failed to fetch live transaction data", error);
|
||||
notifyError("Unable to load live transaction data. Displaying placeholders.");
|
||||
console.error("[Explorer] Failed to fetch live transaction data", error);
|
||||
notifyError("Unable to load transactions from coordinator. Showing placeholders.");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -68,15 +103,15 @@ export async function fetchAddresses(): Promise<AddressSummary[]> {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${CONFIG.apiBaseUrl}/v1/addresses`);
|
||||
const response = await fetch(`${CONFIG.apiBaseUrl}/v1/explorer/addresses`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch addresses: ${response.status}`);
|
||||
throw new Error(`Failed to fetch addresses: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const data = (await response.json()) as { items: AddressDetailResponse[] } | AddressDetailResponse[];
|
||||
return Array.isArray(data) ? data : data.items;
|
||||
const data = (await response.json()) as AddressListResponse;
|
||||
return data.items;
|
||||
} catch (error) {
|
||||
console.warn("[Explorer] Failed to fetch live address data", error);
|
||||
notifyError("Unable to load live address data. Displaying placeholders.");
|
||||
console.error("[Explorer] Failed to fetch live address data", error);
|
||||
notifyError("Unable to load address summaries from coordinator. Showing placeholders.");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -88,15 +123,15 @@ export async function fetchReceipts(): Promise<ReceiptSummary[]> {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${CONFIG.apiBaseUrl}/v1/receipts`);
|
||||
const response = await fetch(`${CONFIG.apiBaseUrl}/v1/explorer/receipts`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch receipts: ${response.status}`);
|
||||
throw new Error(`Failed to fetch receipts: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const data = (await response.json()) as ReceiptListResponse;
|
||||
return data.items;
|
||||
} catch (error) {
|
||||
console.warn("[Explorer] Failed to fetch live receipt data", error);
|
||||
notifyError("Unable to load live receipt data. Displaying placeholders.");
|
||||
console.error("[Explorer] Failed to fetch live receipt data", error);
|
||||
notifyError("Unable to load receipts from coordinator. Showing placeholders.");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
8
apps/explorer-web/test-results/.last-run.json
Normal file
8
apps/explorer-web/test-results/.last-run.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"status": "failed",
|
||||
"failedTests": [
|
||||
"78c83d26793c923d7fe5-1c82705bd81364a8b68d",
|
||||
"78c83d26793c923d7fe5-d8983ad99256a494df4f",
|
||||
"78c83d26793c923d7fe5-a5eb02c7b1bcc34f643e"
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
# Page snapshot
|
||||
|
||||
```yaml
|
||||
- generic [ref=e2]:
|
||||
- main [ref=e3]:
|
||||
- generic [ref=e4]:
|
||||
- paragraph [ref=e5]:
|
||||
- text: "Data mode:"
|
||||
- strong [ref=e6]: MOCK
|
||||
- heading "Marketplace Control Center" [level=1] [ref=e7]
|
||||
- paragraph [ref=e8]: Monitor available offers, submit bids, and review marketplace health at a glance.
|
||||
- generic [ref=e9]:
|
||||
- article [ref=e10]:
|
||||
- heading "Total Offers" [level=2] [ref=e11]
|
||||
- strong [ref=e12]: "78"
|
||||
- generic [ref=e13]: Listings currently visible
|
||||
- article [ref=e14]:
|
||||
- heading "Open Capacity" [level=2] [ref=e15]
|
||||
- strong [ref=e16]: 1,120 units
|
||||
- generic [ref=e17]: GPU / compute units available
|
||||
- article [ref=e18]:
|
||||
- heading "Average Price" [level=2] [ref=e19]
|
||||
- strong [ref=e20]: 14.30 credits
|
||||
- generic [ref=e21]: Credits per unit per hour
|
||||
- article [ref=e22]:
|
||||
- heading "Active Bids" [level=2] [ref=e23]
|
||||
- strong [ref=e24]: "36"
|
||||
- generic [ref=e25]: Open bids awaiting match
|
||||
- generic [ref=e26]:
|
||||
- article [ref=e27]:
|
||||
- heading "Available Offers" [level=2] [ref=e28]
|
||||
- table [ref=e31]:
|
||||
- rowgroup [ref=e32]:
|
||||
- row "ID Provider Capacity Price SLA Status" [ref=e33]:
|
||||
- cell "ID" [ref=e34]
|
||||
- cell "Provider" [ref=e35]
|
||||
- cell "Capacity" [ref=e36]
|
||||
- cell "Price" [ref=e37]
|
||||
- cell "SLA" [ref=e38]
|
||||
- cell "Status" [ref=e39]
|
||||
- rowgroup [ref=e40]:
|
||||
- row "offer-101 Alpha Pool 250 units 12.50 99.9% Open" [ref=e41]:
|
||||
- cell "offer-101" [ref=e42]
|
||||
- cell "Alpha Pool" [ref=e43]
|
||||
- cell "250 units" [ref=e44]
|
||||
- cell "12.50" [ref=e45]
|
||||
- cell "99.9%" [ref=e46]
|
||||
- cell "Open" [ref=e47]:
|
||||
- generic [ref=e48]: Open
|
||||
- row "offer-102 Beta Collective 140 units 15.75 99.5% Open" [ref=e49]:
|
||||
- cell "offer-102" [ref=e50]
|
||||
- cell "Beta Collective" [ref=e51]
|
||||
- cell "140 units" [ref=e52]
|
||||
- cell "15.75" [ref=e53]
|
||||
- cell "99.5%" [ref=e54]
|
||||
- cell "Open" [ref=e55]:
|
||||
- generic [ref=e56]: Open
|
||||
- row "offer-103 Gamma Compute 400 units 10.90 99.95% Reserved" [ref=e57]:
|
||||
- cell "offer-103" [ref=e58]
|
||||
- cell "Gamma Compute" [ref=e59]
|
||||
- cell "400 units" [ref=e60]
|
||||
- cell "10.90" [ref=e61]
|
||||
- cell "99.95%" [ref=e62]
|
||||
- cell "Reserved" [ref=e63]:
|
||||
- generic [ref=e64]: Reserved
|
||||
- row "offer-104 Delta Grid 90 units 18.25 99.0% Open" [ref=e65]:
|
||||
- cell "offer-104" [ref=e66]
|
||||
- cell "Delta Grid" [ref=e67]
|
||||
- cell "90 units" [ref=e68]
|
||||
- cell "18.25" [ref=e69]
|
||||
- cell "99.0%" [ref=e70]
|
||||
- cell "Open" [ref=e71]:
|
||||
- generic [ref=e72]: Open
|
||||
- article [ref=e73]:
|
||||
- heading "Submit a Bid" [level=2] [ref=e74]
|
||||
- generic [ref=e75]:
|
||||
- generic [ref=e76]:
|
||||
- generic [ref=e77]: Preferred provider
|
||||
- textbox "Preferred provider" [ref=e78]
|
||||
- generic [ref=e79]:
|
||||
- generic [ref=e80]: Capacity required (units)
|
||||
- spinbutton "Capacity required (units)" [ref=e81]
|
||||
- generic [ref=e82]:
|
||||
- generic [ref=e83]: Bid price (credits/unit/hr)
|
||||
- spinbutton "Bid price (credits/unit/hr)" [ref=e84]
|
||||
- generic [ref=e85]:
|
||||
- generic [ref=e86]: Notes (optional)
|
||||
- textbox "Notes (optional)" [ref=e87]
|
||||
- button "Submit Bid" [ref=e88] [cursor=pointer]
|
||||
- complementary [ref=e89]
|
||||
```
|
||||
@ -0,0 +1,91 @@
|
||||
# Page snapshot
|
||||
|
||||
```yaml
|
||||
- generic [ref=e2]:
|
||||
- main [ref=e3]:
|
||||
- generic [ref=e4]:
|
||||
- paragraph [ref=e5]:
|
||||
- text: "Data mode:"
|
||||
- strong [ref=e6]: MOCK
|
||||
- heading "Marketplace Control Center" [level=1] [ref=e7]
|
||||
- paragraph [ref=e8]: Monitor available offers, submit bids, and review marketplace health at a glance.
|
||||
- generic [ref=e9]:
|
||||
- article [ref=e10]:
|
||||
- heading "Total Offers" [level=2] [ref=e11]
|
||||
- strong [ref=e12]: "78"
|
||||
- generic [ref=e13]: Listings currently visible
|
||||
- article [ref=e14]:
|
||||
- heading "Open Capacity" [level=2] [ref=e15]
|
||||
- strong [ref=e16]: 1,120 units
|
||||
- generic [ref=e17]: GPU / compute units available
|
||||
- article [ref=e18]:
|
||||
- heading "Average Price" [level=2] [ref=e19]
|
||||
- strong [ref=e20]: 14.30 credits
|
||||
- generic [ref=e21]: Credits per unit per hour
|
||||
- article [ref=e22]:
|
||||
- heading "Active Bids" [level=2] [ref=e23]
|
||||
- strong [ref=e24]: "36"
|
||||
- generic [ref=e25]: Open bids awaiting match
|
||||
- generic [ref=e26]:
|
||||
- article [ref=e27]:
|
||||
- heading "Available Offers" [level=2] [ref=e28]
|
||||
- table [ref=e31]:
|
||||
- rowgroup [ref=e32]:
|
||||
- row "ID Provider Capacity Price SLA Status" [ref=e33]:
|
||||
- cell "ID" [ref=e34]
|
||||
- cell "Provider" [ref=e35]
|
||||
- cell "Capacity" [ref=e36]
|
||||
- cell "Price" [ref=e37]
|
||||
- cell "SLA" [ref=e38]
|
||||
- cell "Status" [ref=e39]
|
||||
- rowgroup [ref=e40]:
|
||||
- row "offer-101 Alpha Pool 250 units 12.50 99.9% Open" [ref=e41]:
|
||||
- cell "offer-101" [ref=e42]
|
||||
- cell "Alpha Pool" [ref=e43]
|
||||
- cell "250 units" [ref=e44]
|
||||
- cell "12.50" [ref=e45]
|
||||
- cell "99.9%" [ref=e46]
|
||||
- cell "Open" [ref=e47]:
|
||||
- generic [ref=e48]: Open
|
||||
- row "offer-102 Beta Collective 140 units 15.75 99.5% Open" [ref=e49]:
|
||||
- cell "offer-102" [ref=e50]
|
||||
- cell "Beta Collective" [ref=e51]
|
||||
- cell "140 units" [ref=e52]
|
||||
- cell "15.75" [ref=e53]
|
||||
- cell "99.5%" [ref=e54]
|
||||
- cell "Open" [ref=e55]:
|
||||
- generic [ref=e56]: Open
|
||||
- row "offer-103 Gamma Compute 400 units 10.90 99.95% Reserved" [ref=e57]:
|
||||
- cell "offer-103" [ref=e58]
|
||||
- cell "Gamma Compute" [ref=e59]
|
||||
- cell "400 units" [ref=e60]
|
||||
- cell "10.90" [ref=e61]
|
||||
- cell "99.95%" [ref=e62]
|
||||
- cell "Reserved" [ref=e63]:
|
||||
- generic [ref=e64]: Reserved
|
||||
- row "offer-104 Delta Grid 90 units 18.25 99.0% Open" [ref=e65]:
|
||||
- cell "offer-104" [ref=e66]
|
||||
- cell "Delta Grid" [ref=e67]
|
||||
- cell "90 units" [ref=e68]
|
||||
- cell "18.25" [ref=e69]
|
||||
- cell "99.0%" [ref=e70]
|
||||
- cell "Open" [ref=e71]:
|
||||
- generic [ref=e72]: Open
|
||||
- article [ref=e73]:
|
||||
- heading "Submit a Bid" [level=2] [ref=e74]
|
||||
- generic [ref=e75]:
|
||||
- generic [ref=e76]:
|
||||
- generic [ref=e77]: Preferred provider
|
||||
- textbox "Preferred provider" [ref=e78]
|
||||
- generic [ref=e79]:
|
||||
- generic [ref=e80]: Capacity required (units)
|
||||
- spinbutton "Capacity required (units)" [ref=e81]
|
||||
- generic [ref=e82]:
|
||||
- generic [ref=e83]: Bid price (credits/unit/hr)
|
||||
- spinbutton "Bid price (credits/unit/hr)" [ref=e84]
|
||||
- generic [ref=e85]:
|
||||
- generic [ref=e86]: Notes (optional)
|
||||
- textbox "Notes (optional)" [ref=e87]
|
||||
- button "Submit Bid" [ref=e88] [cursor=pointer]
|
||||
- complementary [ref=e89]
|
||||
```
|
||||
@ -0,0 +1,91 @@
|
||||
# Page snapshot
|
||||
|
||||
```yaml
|
||||
- generic [ref=e2]:
|
||||
- main [ref=e3]:
|
||||
- generic [ref=e4]:
|
||||
- paragraph [ref=e5]:
|
||||
- text: "Data mode:"
|
||||
- strong [ref=e6]: MOCK
|
||||
- heading "Marketplace Control Center" [level=1] [ref=e7]
|
||||
- paragraph [ref=e8]: Monitor available offers, submit bids, and review marketplace health at a glance.
|
||||
- generic [ref=e9]:
|
||||
- article [ref=e10]:
|
||||
- heading "Total Offers" [level=2] [ref=e11]
|
||||
- strong [ref=e12]: "78"
|
||||
- generic [ref=e13]: Listings currently visible
|
||||
- article [ref=e14]:
|
||||
- heading "Open Capacity" [level=2] [ref=e15]
|
||||
- strong [ref=e16]: 1,120 units
|
||||
- generic [ref=e17]: GPU / compute units available
|
||||
- article [ref=e18]:
|
||||
- heading "Average Price" [level=2] [ref=e19]
|
||||
- strong [ref=e20]: 14.30 credits
|
||||
- generic [ref=e21]: Credits per unit per hour
|
||||
- article [ref=e22]:
|
||||
- heading "Active Bids" [level=2] [ref=e23]
|
||||
- strong [ref=e24]: "36"
|
||||
- generic [ref=e25]: Open bids awaiting match
|
||||
- generic [ref=e26]:
|
||||
- article [ref=e27]:
|
||||
- heading "Available Offers" [level=2] [ref=e28]
|
||||
- table [ref=e31]:
|
||||
- rowgroup [ref=e32]:
|
||||
- row "ID Provider Capacity Price SLA Status" [ref=e33]:
|
||||
- cell "ID" [ref=e34]
|
||||
- cell "Provider" [ref=e35]
|
||||
- cell "Capacity" [ref=e36]
|
||||
- cell "Price" [ref=e37]
|
||||
- cell "SLA" [ref=e38]
|
||||
- cell "Status" [ref=e39]
|
||||
- rowgroup [ref=e40]:
|
||||
- row "offer-101 Alpha Pool 250 units 12.50 99.9% Open" [ref=e41]:
|
||||
- cell "offer-101" [ref=e42]
|
||||
- cell "Alpha Pool" [ref=e43]
|
||||
- cell "250 units" [ref=e44]
|
||||
- cell "12.50" [ref=e45]
|
||||
- cell "99.9%" [ref=e46]
|
||||
- cell "Open" [ref=e47]:
|
||||
- generic [ref=e48]: Open
|
||||
- row "offer-102 Beta Collective 140 units 15.75 99.5% Open" [ref=e49]:
|
||||
- cell "offer-102" [ref=e50]
|
||||
- cell "Beta Collective" [ref=e51]
|
||||
- cell "140 units" [ref=e52]
|
||||
- cell "15.75" [ref=e53]
|
||||
- cell "99.5%" [ref=e54]
|
||||
- cell "Open" [ref=e55]:
|
||||
- generic [ref=e56]: Open
|
||||
- row "offer-103 Gamma Compute 400 units 10.90 99.95% Reserved" [ref=e57]:
|
||||
- cell "offer-103" [ref=e58]
|
||||
- cell "Gamma Compute" [ref=e59]
|
||||
- cell "400 units" [ref=e60]
|
||||
- cell "10.90" [ref=e61]
|
||||
- cell "99.95%" [ref=e62]
|
||||
- cell "Reserved" [ref=e63]:
|
||||
- generic [ref=e64]: Reserved
|
||||
- row "offer-104 Delta Grid 90 units 18.25 99.0% Open" [ref=e65]:
|
||||
- cell "offer-104" [ref=e66]
|
||||
- cell "Delta Grid" [ref=e67]
|
||||
- cell "90 units" [ref=e68]
|
||||
- cell "18.25" [ref=e69]
|
||||
- cell "99.0%" [ref=e70]
|
||||
- cell "Open" [ref=e71]:
|
||||
- generic [ref=e72]: Open
|
||||
- article [ref=e73]:
|
||||
- heading "Submit a Bid" [level=2] [ref=e74]
|
||||
- generic [ref=e75]:
|
||||
- generic [ref=e76]:
|
||||
- generic [ref=e77]: Preferred provider
|
||||
- textbox "Preferred provider" [ref=e78]
|
||||
- generic [ref=e79]:
|
||||
- generic [ref=e80]: Capacity required (units)
|
||||
- spinbutton "Capacity required (units)" [ref=e81]
|
||||
- generic [ref=e82]:
|
||||
- generic [ref=e83]: Bid price (credits/unit/hr)
|
||||
- spinbutton "Bid price (credits/unit/hr)" [ref=e84]
|
||||
- generic [ref=e85]:
|
||||
- generic [ref=e86]: Notes (optional)
|
||||
- textbox "Notes (optional)" [ref=e87]
|
||||
- button "Submit Bid" [ref=e88] [cursor=pointer]
|
||||
- complementary [ref=e89]
|
||||
```
|
||||
111
apps/explorer-web/tests/e2e/explorer-live.spec.ts
Normal file
111
apps/explorer-web/tests/e2e/explorer-live.spec.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Explorer live mode", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("aitbc-explorer:data-mode", "live");
|
||||
});
|
||||
|
||||
await page.route("**/v1/explorer/blocks", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
height: 12345,
|
||||
hash: "0xabcdef1234567890",
|
||||
timestamp: new Date("2024-08-22T12:00:00Z").toISOString(),
|
||||
txCount: 12,
|
||||
proposer: "validator-1",
|
||||
},
|
||||
],
|
||||
next_offset: null,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/v1/explorer/transactions", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
hash: "0xfeed1234",
|
||||
block: 12345,
|
||||
from: "0xAAA",
|
||||
to: "0xBBB",
|
||||
value: "0.50",
|
||||
status: "Succeeded",
|
||||
},
|
||||
],
|
||||
next_offset: null,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/v1/explorer/receipts", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
jobId: "job-1",
|
||||
items: [
|
||||
{
|
||||
receiptId: "receipt-1",
|
||||
miner: "miner-1",
|
||||
coordinator: "coordinator-1",
|
||||
issuedAt: new Date("2024-08-22T12:00:00Z").toISOString(),
|
||||
status: "Attested",
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/v1/explorer/addresses", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
address: "0xADDRESS",
|
||||
balance: "100.0",
|
||||
txCount: 42,
|
||||
lastActive: new Date("2024-08-22T10:00:00Z").toISOString(),
|
||||
},
|
||||
],
|
||||
next_offset: null,
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("overview renders live summaries", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.locator("#overview-block-stats")).toContainText("12345");
|
||||
await expect(page.locator("#overview-transaction-stats")).toContainText("Total Mock Tx: 1");
|
||||
await expect(page.locator("#overview-receipt-stats")).toContainText("Total Receipts: 1");
|
||||
});
|
||||
|
||||
test("blocks table shows live rows", async ({ page }) => {
|
||||
await page.goto("/blocks");
|
||||
|
||||
const rows = page.locator("#blocks-table-body tr");
|
||||
await expect(rows).toHaveCount(1);
|
||||
await expect(rows.first()).toContainText("12345");
|
||||
await expect(rows.first()).toContainText("validator-1");
|
||||
});
|
||||
|
||||
test("transactions table shows live rows", async ({ page }) => {
|
||||
await page.goto("/transactions");
|
||||
|
||||
const rows = page.locator("tbody tr");
|
||||
await expect(rows).toHaveCount(1);
|
||||
await expect(rows.first()).toContainText("0xfeed1234");
|
||||
await expect(rows.first()).toContainText("Succeeded");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user