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:
oib
2025-12-22 07:55:09 +01:00
parent fb60505cdf
commit d98b2c7772
70 changed files with 3472 additions and 246 deletions

View File

@ -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 500ms, max 5attempts).
- 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.

View File

@ -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"
}

View 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"] },
},
],
});

View File

@ -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 {

View File

@ -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 [];
}
}

View File

@ -0,0 +1,8 @@
{
"status": "failed",
"failedTests": [
"78c83d26793c923d7fe5-1c82705bd81364a8b68d",
"78c83d26793c923d7fe5-d8983ad99256a494df4f",
"78c83d26793c923d7fe5-a5eb02c7b1bcc34f643e"
]
}

View File

@ -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]
```

View File

@ -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]
```

View File

@ -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]
```

View 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");
});
});