refactor: consolidate blockchain explorer into single app and update backup ignore patterns

- Remove standalone explorer-web app (README, HTML, package files)
- Add /web endpoint to blockchain-explorer for web interface access
- Update .gitignore to exclude application backup archives (*.tar.gz, *.zip)
- Add backup documentation files to .gitignore (BACKUP_INDEX.md, README.md)
- Consolidate explorer functionality into main blockchain-explorer application
This commit is contained in:
oib
2026-03-06 18:14:49 +01:00
parent dc1561d457
commit bb5363bebc
295 changed files with 35501 additions and 3734 deletions

View File

@@ -0,0 +1,273 @@
// Type declarations for global objects
declare global {
interface Window {
analytics?: {
track: (event: string, data: any) => void;
};
}
}
import './style.css';
import {
fetchMarketplaceOffers,
fetchMarketplaceStats,
submitMarketplaceBid,
} from './lib/api';
import type { MarketplaceOffer, MarketplaceStats } from './lib/api';
const app = document.querySelector<HTMLDivElement>('#app');
if (!app) {
throw new Error('Unable to mount marketplace app');
}
app.innerHTML = `
<main>
<section class="dashboard-grid" id="stats-panel">
<article class="stat-card">
<h2>Total Offers</h2>
<strong id="stat-total-offers">--</strong>
<span>Listings currently visible</span>
</article>
<article class="stat-card">
<h2>Open Capacity</h2>
<strong id="stat-open-capacity">--</strong>
<span>GPU / compute units available</span>
</article>
<article class="stat-card">
<h2>Average Price</h2>
<strong id="stat-average-price">--</strong>
<span>Credits per unit per hour</span>
</article>
<article class="stat-card">
<h2>Active Bids</h2>
<strong id="stat-active-bids">--</strong>
<span>Open bids awaiting match</span>
</article>
</section>
<section class="panels">
<article class="panel" id="offers-panel">
<h2>Available Offers</h2>
<div id="offers-table-wrapper" class="table-wrapper">
<p class="empty-state">Fetching marketplace offers…</p>
</div>
</article>
<article class="panel">
<h2>Submit a Bid</h2>
<form class="bid-form" id="bid-form">
<div>
<label for="bid-provider">Preferred provider</label>
<input id="bid-provider" name="provider" placeholder="Alpha Pool" required />
</div>
<div>
<label for="bid-capacity">Capacity required (units)</label>
<input id="bid-capacity" name="capacity" type="number" min="1" step="1" required />
</div>
<div>
<label for="bid-price">Bid price (credits/unit/hr)</label>
<input id="bid-price" name="price" type="number" min="0" step="0.01" required />
</div>
<div>
<label for="bid-notes">Notes (optional)</label>
<textarea id="bid-notes" name="notes" rows="3" placeholder="Add constraints, time windows, etc."></textarea>
</div>
<button type="submit">Submit Bid</button>
</form>
</article>
</section>
</main>
<aside id="toast" class="toast"></aside>
`;
const selectors = {
totalOffers: document.querySelector<HTMLSpanElement>('#stat-total-offers')!,
openCapacity: document.querySelector<HTMLSpanElement>('#stat-open-capacity')!,
averagePrice: document.querySelector<HTMLSpanElement>('#stat-average-price')!,
activeBids: document.querySelector<HTMLSpanElement>('#stat-active-bids')!,
statsWrapper: document.querySelector<HTMLDivElement>('#stats-grid')!,
offersWrapper: document.querySelector<HTMLDivElement>('#offers-table-wrapper')!,
bidForm: document.querySelector<HTMLFormElement>('#bid-form')!,
toast: document.querySelector<HTMLDivElement>('#toast')!,
};
function formatNumber(value: number, options: Intl.NumberFormatOptions = {}): string {
return new Intl.NumberFormat(undefined, options).format(value);
}
function renderStats(stats: MarketplaceStats): void {
selectors.totalOffers.textContent = formatNumber(stats.totalOffers);
selectors.openCapacity.textContent = `${formatNumber(stats.openCapacity)} units`;
selectors.averagePrice.textContent = `${formatNumber(stats.averagePrice, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})} credits`;
selectors.activeBids.textContent = formatNumber(stats.activeBids);
}
function statusClass(status: string): string {
switch (status.toLowerCase()) {
case 'open':
return 'status-pill status-open';
case 'reserved':
return 'status-pill status-reserved';
default:
return 'status-pill';
}
}
function renderOffers(offers: MarketplaceOffer[]): void {
const wrapper = selectors.offersWrapper;
if (!wrapper) return;
if (offers.length === 0) {
wrapper.innerHTML = '<p class="empty-state">No offers available right now. Check back soon or submit a bid.</p>';
return;
}
const cards = offers
.map(
(offer) => `
<article class="offer-card">
<div class="offer-card-header">
<div class="offer-gpu-name">${offer.gpu_model || 'Unknown GPU'}</div>
<span class="${statusClass(offer.status)}">${offer.status}</span>
</div>
<div class="offer-provider">${offer.provider}${offer.region ? ` · ${offer.region}` : ''}</div>
<div class="offer-specs">
<div class="spec-item">
<span class="spec-label">VRAM</span>
<span class="spec-value">${offer.gpu_memory_gb ? `${offer.gpu_memory_gb} GB` : '—'}</span>
</div>
<div class="spec-item">
<span class="spec-label">GPUs</span>
<span class="spec-value">${offer.gpu_count ?? 1}×</span>
</div>
<div class="spec-item">
<span class="spec-label">CUDA</span>
<span class="spec-value">${offer.cuda_version || '—'}</span>
</div>
<div class="spec-item">
<span class="spec-label">Capacity</span>
<span class="spec-value">${formatNumber(offer.capacity)} units</span>
</div>
</div>
${offer.attributes?.models?.length ? `
<div class="offer-plugins">
<span class="plugin-badge">Ollama</span>
</div>
<div class="offer-models">
<span class="models-label">Available Models</span>
<div class="model-tags">${offer.attributes.models.map((m: string) => `<span class="model-tag">${m}</span>`).join('')}</div>
</div>` : ''}
<div class="offer-pricing">
<div class="offer-price">${formatNumber(offer.price_per_hour ?? offer.price, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} <small>credits/hr</small></div>
<div class="offer-sla">${offer.sla}</div>
</div>
</article>
`,
)
.join('');
wrapper.innerHTML = `<div class="offers-grid">${cards}</div>`;
}
function showToast(message: string, duration = 2500): void {
if (!selectors.toast) return;
selectors.toast.textContent = message;
selectors.toast.classList.add('visible');
window.setTimeout(() => {
selectors.toast?.classList.remove('visible');
}, duration);
}
async function loadDashboard(): Promise<void> {
// Show skeleton loading states
showSkeletons();
try {
const [stats, offers] = await Promise.all([
fetchMarketplaceStats(),
fetchMarketplaceOffers(),
]);
renderStats(stats);
renderOffers(offers);
} catch (error) {
console.error(error);
const wrapper = selectors.offersWrapper;
if (wrapper) {
wrapper.innerHTML = '<p class="empty-state">Failed to load offers. Please retry shortly.</p>';
}
showToast('Failed to load marketplace data.');
}
}
function showSkeletons() {
const statsWrapper = selectors.statsWrapper;
const offersWrapper = selectors.offersWrapper;
if (statsWrapper) {
statsWrapper.innerHTML = `
<div class="skeleton-grid">
${Array(4).fill('').map(() => `
<div class="skeleton skeleton-card"></div>
`).join('')}
</div>
`;
}
if (offersWrapper) {
offersWrapper.innerHTML = `
<div class="skeleton-list">
${Array(6).fill('').map(() => `
<div class="skeleton skeleton-card"></div>
`).join('')}
</div>
`;
}
}
selectors.bidForm?.addEventListener('submit', async (event) => {
event.preventDefault();
const form = selectors.bidForm;
if (!form) return;
const formData = new FormData(form);
const provider = formData.get('provider')?.toString().trim();
const capacity = Number(formData.get('capacity'));
const price = Number(formData.get('price'));
const notes = formData.get('notes')?.toString().trim();
if (!provider || Number.isNaN(capacity) || Number.isNaN(price)) {
showToast('Please complete the required fields.');
return;
}
try {
const submitButton = form.querySelector('button');
if (submitButton) {
submitButton.setAttribute('disabled', 'disabled');
}
await submitMarketplaceBid({ provider, capacity, price, notes });
form.reset();
showToast('Bid submitted successfully!');
} catch (error) {
console.error(error);
showToast('Unable to submit bid. Please try again.');
} finally {
const submitButton = form.querySelector('button');
if (submitButton) {
submitButton.removeAttribute('disabled');
}
}
});
loadDashboard();