diff --git a/apps/coordinator-api/src/app/config.py b/apps/coordinator-api/src/app/config.py index 2340ebef..b39f7044 100644 --- a/apps/coordinator-api/src/app/config.py +++ b/apps/coordinator-api/src/app/config.py @@ -18,35 +18,12 @@ class DatabaseConfig(BaseSettings): max_overflow: int = 20 pool_pre_ping: bool = True - @property - def effective_url(self) -> str: - """Get the effective database URL.""" - if self.url: - return self.url - # Auto-generate SQLite URL based on environment - if self.adapter == "sqlite": - project_root = self._find_project_root() - db_path = project_root / "data" / "coordinator.db" - db_path.parent.mkdir(parents=True, exist_ok=True) - return f"sqlite:///{db_path}" - elif self.adapter == "postgresql": - return "postgresql://localhost:5432/aitbc_coordinator" - return "sqlite:///:memory:" - - @staticmethod - def _find_project_root() -> Path: - """Find project root by looking for .git directory.""" - current = Path(__file__).resolve() - while current.parent != current: - if (current / ".git").exists(): - return current - current = current.parent - return Path(__file__).resolve().parents[3] - - class Config: - env_file = ".env" - env_file_encoding = "utf-8" - case_sensitive = False + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + case_sensitive=False, + extra="allow" + ) class Settings(BaseSettings): @@ -116,7 +93,10 @@ class Settings(BaseSettings): @property def database_url(self) -> str: """Get the database URL (backward compatibility).""" - return self.database.effective_url + if self.database.url: + return self.database.url + # Default SQLite path for backward compatibility + return f"sqlite:///./aitbc_coordinator.db" settings = Settings() diff --git a/apps/coordinator-api/src/app/main.py b/apps/coordinator-api/src/app/main.py index 7bbe86ab..ad2daf94 100644 --- a/apps/coordinator-api/src/app/main.py +++ b/apps/coordinator-api/src/app/main.py @@ -19,6 +19,7 @@ from .routers import ( zk_applications, explorer, payments, + web_vitals, ) from .routers.governance import router as governance from .routers.partners import router as partners @@ -75,6 +76,7 @@ def create_app() -> FastAPI: app.include_router(governance, prefix="/v1") app.include_router(partners, prefix="/v1") app.include_router(explorer, prefix="/v1") + app.include_router(web_vitals, prefix="/v1") # Add Prometheus metrics endpoint metrics_app = make_asgi_app() diff --git a/apps/coordinator-api/src/app/routers/__init__.py b/apps/coordinator-api/src/app/routers/__init__.py index 083e8b46..15fc7fb7 100644 --- a/apps/coordinator-api/src/app/routers/__init__.py +++ b/apps/coordinator-api/src/app/routers/__init__.py @@ -11,6 +11,7 @@ from .users import router as users from .exchange import router as exchange from .marketplace_offers import router as marketplace_offers from .payments import router as payments +from .web_vitals import router as web_vitals # from .registry import router as registry -__all__ = ["client", "miner", "admin", "marketplace", "marketplace_gpu", "explorer", "services", "users", "exchange", "marketplace_offers", "payments", "registry"] +__all__ = ["client", "miner", "admin", "marketplace", "marketplace_gpu", "explorer", "services", "users", "exchange", "marketplace_offers", "payments", "web_vitals", "registry"] diff --git a/apps/coordinator-api/src/app/routers/web_vitals.py b/apps/coordinator-api/src/app/routers/web_vitals.py new file mode 100644 index 00000000..2c1c13f6 --- /dev/null +++ b/apps/coordinator-api/src/app/routers/web_vitals.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +""" +Web Vitals API endpoint for collecting performance metrics +""" + +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from typing import List, Dict, Any, Optional +import logging + +router = APIRouter() + +class WebVitalsEntry(BaseModel): + name: str + startTime: Optional[float] = None + duration: Optional[float] = None + +class WebVitalsMetric(BaseModel): + name: str + value: float + id: str + delta: Optional[float] = None + entries: List[WebVitalsEntry] = [] + url: Optional[str] = None + timestamp: Optional[str] = None + +@router.post("/web-vitals") +async def collect_web_vitals(metric: WebVitalsMetric): + """ + Collect Web Vitals performance metrics from the frontend. + This endpoint receives Core Web Vitals (LCP, FID, CLS, TTFB, FCP) for monitoring. + """ + try: + # Log the metric for monitoring/analysis + logging.info(f"Web Vitals - {metric.name}: {metric.value}ms (ID: {metric.id}) from {metric.url or 'unknown'}") + + # In a production setup, you might: + # - Store in database for trend analysis + # - Send to monitoring service (DataDog, New Relic, etc.) + # - Trigger alerts for poor performance + + # For now, just acknowledge receipt + return {"status": "received", "metric": metric.name, "value": metric.value} + + except Exception as e: + logging.error(f"Error processing web vitals metric: {e}") + raise HTTPException(status_code=500, detail="Failed to process metric") + +# Health check for web vitals endpoint +@router.get("/web-vitals/health") +async def web_vitals_health(): + """Health check for web vitals collection endpoint""" + return {"status": "healthy", "service": "web-vitals"} diff --git a/apps/coordinator-api/src/app/services/explorer.py b/apps/coordinator-api/src/app/services/explorer.py index 0c4a7b07..e5260b92 100644 --- a/apps/coordinator-api/src/app/services/explorer.py +++ b/apps/coordinator-api/src/app/services/explorer.py @@ -70,7 +70,8 @@ class ExplorerService: items: list[TransactionSummary] = [] for index, job in enumerate(jobs): height = _DEFAULT_HEIGHT_BASE + offset + index - status_label = _STATUS_LABELS.get(job.state, job.state.value.title()) + state_val = job.state.value if hasattr(job.state, "value") else job.state + status_label = _STATUS_LABELS.get(job.state) or state_val.title() # Try to get payment amount from receipt value_str = "0" @@ -118,14 +119,26 @@ class ExplorerService: } ) - def touch(address: Optional[str], tx_id: str, when: datetime, earned: float = 0.0, spent: float = 0.0) -> None: + def _ensure_dt(val: object) -> datetime: + if isinstance(val, datetime): + return val.replace(tzinfo=None) + if isinstance(val, str): + try: + dt = datetime.fromisoformat(val.replace("Z", "+00:00")) + return dt.replace(tzinfo=None) + except ValueError: + return datetime.min + return datetime.min + + def touch(address: Optional[str], tx_id: str, when: object, earned: float = 0.0, spent: float = 0.0) -> None: if not address: return entry = address_map[address] entry["address"] = address entry["tx_count"] = int(entry["tx_count"]) + 1 - if when > entry["last_active"]: - entry["last_active"] = when + when_dt = _ensure_dt(when) + if when_dt > _ensure_dt(entry["last_active"]): + entry["last_active"] = when_dt # Track earnings and spending entry["earned"] = float(entry["earned"]) + earned entry["spent"] = float(entry["spent"]) + spent diff --git a/apps/explorer-web/public/css/layout.css b/apps/explorer-web/public/css/layout.css index 0163fcd0..421624e2 100644 --- a/apps/explorer-web/public/css/layout.css +++ b/apps/explorer-web/public/css/layout.css @@ -95,6 +95,20 @@ gap: 1rem; } +.site-header__back { + font-size: 0.85rem; + padding: 0.25rem 0.6rem; + border-radius: 999px; + border: 1px solid rgba(125, 196, 255, 0.3); + transition: background 150ms ease, border-color 150ms ease; + white-space: nowrap; +} + +.site-header__back:hover { + background: rgba(125, 196, 255, 0.15); + border-color: rgba(125, 196, 255, 0.5); +} + .site-header__brand { font-weight: 600; font-size: 1.15rem; diff --git a/apps/explorer-web/src/components/siteHeader.ts b/apps/explorer-web/src/components/siteHeader.ts index fcb3d711..c732cd8d 100644 --- a/apps/explorer-web/src/components/siteHeader.ts +++ b/apps/explorer-web/src/components/siteHeader.ts @@ -4,6 +4,7 @@ export function siteHeader(title: string): string { return ` diff --git a/apps/marketplace-web/src/main.ts b/apps/marketplace-web/src/main.ts index 82b4e1ff..5eed7de9 100644 --- a/apps/marketplace-web/src/main.ts +++ b/apps/marketplace-web/src/main.ts @@ -1,9 +1,17 @@ +// Type declarations for global objects +declare global { + interface Window { + analytics?: { + track: (event: string, data: any) => void; + }; + } +} + import './style.css'; import { fetchMarketplaceOffers, fetchMarketplaceStats, submitMarketplaceBid, - MARKETPLACE_CONFIG, } from './lib/api'; import type { MarketplaceOffer, MarketplaceStats } from './lib/api'; @@ -16,9 +24,21 @@ if (!app) { app.innerHTML = `
@@ -80,13 +100,13 @@ app.innerHTML = ` `; const selectors = { - totalOffers: document.querySelector('#stat-total-offers'), - openCapacity: document.querySelector('#stat-open-capacity'), - averagePrice: document.querySelector('#stat-average-price'), - activeBids: document.querySelector('#stat-active-bids'), - offersWrapper: document.querySelector('#offers-table-wrapper'), - bidForm: document.querySelector('#bid-form'), - toast: document.querySelector('#toast'), + totalOffers: document.querySelector('#stat-total-offers')!, + openCapacity: document.querySelector('#stat-open-capacity')!, + averagePrice: document.querySelector('#stat-average-price')!, + activeBids: document.querySelector('#stat-active-bids')!, + offersWrapper: document.querySelector('#offers-table-wrapper')!, + bidForm: document.querySelector('#bid-form')!, + toast: document.querySelector('#toast')!, }; function formatNumber(value: number, options: Intl.NumberFormatOptions = {}): string { @@ -94,13 +114,13 @@ function formatNumber(value: number, options: Intl.NumberFormatOptions = {}): st } function renderStats(stats: MarketplaceStats): void { - selectors.totalOffers!.textContent = formatNumber(stats.totalOffers); - selectors.openCapacity!.textContent = `${formatNumber(stats.openCapacity)} units`; - selectors.averagePrice!.textContent = `${formatNumber(stats.averagePrice, { + 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); + selectors.activeBids.textContent = formatNumber(stats.activeBids); } function statusClass(status: string): string { @@ -115,12 +135,11 @@ function statusClass(status: string): string { } function renderOffers(offers: MarketplaceOffer[]): void { - if (!selectors.offersWrapper) { - return; - } + const wrapper = selectors.offersWrapper; + if (!wrapper) return; if (offers.length === 0) { - selectors.offersWrapper.innerHTML = '

No offers available right now. Check back soon or submit a bid.

'; + wrapper.innerHTML = '

No offers available right now. Check back soon or submit a bid.

'; return; } @@ -157,7 +176,7 @@ function renderOffers(offers: MarketplaceOffer[]): void {
Available Models -
${offer.attributes.models.map(m => `${m}`).join('')}
+
${offer.attributes.models.map((m: string) => `${m}`).join('')}
` : ''}
${formatNumber(offer.price_per_hour ?? offer.price, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} credits/hr
@@ -168,7 +187,7 @@ function renderOffers(offers: MarketplaceOffer[]): void { ) .join(''); - selectors.offersWrapper.innerHTML = `
${cards}
`; + wrapper.innerHTML = `
${cards}
`; } function showToast(message: string, duration = 2500): void { @@ -192,8 +211,9 @@ async function loadDashboard(): Promise { renderOffers(offers); } catch (error) { console.error(error); - if (selectors.offersWrapper) { - selectors.offersWrapper.innerHTML = '

Failed to load offers. Please retry shortly.

'; + const wrapper = selectors.offersWrapper; + if (wrapper) { + wrapper.innerHTML = '

Failed to load offers. Please retry shortly.

'; } showToast('Failed to load marketplace data.'); } @@ -202,7 +222,10 @@ async function loadDashboard(): Promise { selectors.bidForm?.addEventListener('submit', async (event) => { event.preventDefault(); - const formData = new FormData(selectors.bidForm!); + 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')); @@ -214,16 +237,109 @@ selectors.bidForm?.addEventListener('submit', async (event) => { } try { - selectors.bidForm!.querySelector('button')!.setAttribute('disabled', 'disabled'); + const submitButton = form.querySelector('button'); + if (submitButton) { + submitButton.setAttribute('disabled', 'disabled'); + } + await submitMarketplaceBid({ provider, capacity, price, notes }); - selectors.bidForm!.reset(); + form.reset(); showToast('Bid submitted successfully!'); } catch (error) { console.error(error); showToast('Unable to submit bid. Please try again.'); } finally { - selectors.bidForm!.querySelector('button')!.removeAttribute('disabled'); + const submitButton = form.querySelector('button'); + if (submitButton) { + submitButton.removeAttribute('disabled'); + } } }); loadDashboard(); + +// Dark mode functionality with system preference detection +function toggleDarkMode() { + const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light'; + const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + setTheme(newTheme); +} + +function setTheme(theme: string) { + // Apply theme immediately + if (theme === 'dark') { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + + // Save to localStorage for persistence + localStorage.setItem('marketplaceTheme', theme); + + // Update button display + updateThemeButton(theme); + + // Send analytics event if available + if (typeof window !== 'undefined' && window.analytics) { + window.analytics.track('marketplace_theme_changed', { theme }); + } +} + +function updateThemeButton(theme: string) { + const emoji = document.getElementById('darkModeEmoji'); + const text = document.getElementById('darkModeText'); + + if (emoji && text) { + if (theme === 'dark') { + emoji.textContent = '🌙'; + text.textContent = 'Dark'; + } else { + emoji.textContent = '☀️'; + text.textContent = 'Light'; + } + } +} + +function getPreferredTheme(): string { + // 1. Check localStorage first (user preference for marketplace) + const saved = localStorage.getItem('marketplaceTheme'); + if (saved) { + return saved; + } + + // 2. Check main site preference for consistency + const mainSiteTheme = localStorage.getItem('theme'); + if (mainSiteTheme) { + return mainSiteTheme; + } + + // 3. Check system preference + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + return 'dark'; + } + + // 4. Default to dark (AITBC brand preference) + return 'dark'; +} + +function initializeTheme() { + const theme = getPreferredTheme(); + setTheme(theme); + + // Listen for system preference changes + if (window.matchMedia) { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { + // Only auto-switch if user hasn't manually set a preference + if (!localStorage.getItem('marketplaceTheme') && !localStorage.getItem('theme')) { + setTheme(e.matches ? 'dark' : 'light'); + } + }); + } +} + +// Initialize theme immediately (before DOM loads) +initializeTheme(); + +// Reference to suppress TypeScript "never used" warning +// @ts-ignore - function called from HTML onclick +window.toggleDarkMode = toggleDarkMode; diff --git a/apps/marketplace-web/src/style.css b/apps/marketplace-web/src/style.css index af6eb8f6..9b548835 100644 --- a/apps/marketplace-web/src/style.css +++ b/apps/marketplace-web/src/style.css @@ -8,10 +8,29 @@ -moz-osx-font-smoothing: grayscale; } +/* Dark mode variables */ +.dark { + --bg-primary: #1f2937; + --bg-secondary: #374151; + --bg-card: #111827; + --text-primary: #f9fafb; + --text-secondary: #d1d5db; + --text-muted: #9ca3af; + --border-color: #4b5563; + --hover-bg: #374151; +} + body { margin: 0; min-height: 100vh; background: linear-gradient(180deg, #f7f8fa 0%, #eef1f6 100%); + transition: background-color 0.3s ease; +} + +/* Dark mode body */ +.dark body { + background: linear-gradient(180deg, #1f2937 0%, #111827 100%); + color: var(--text-primary); } #app { @@ -24,17 +43,118 @@ body { margin-bottom: 32px; } +.page-header__nav { + display: flex; + gap: 0.75rem; + margin-bottom: 1rem; + align-items: center; +} + +.page-header__nav a { + font-size: 0.85rem; + padding: 0.3rem 0.75rem; + border-radius: 999px; + color: #5a6575; + text-decoration: none; + transition: background 150ms ease, color 150ms ease; +} + +.dark .page-header__nav a { + color: var(--text-secondary); +} + +.page-header__nav a:hover { + background: rgba(37, 99, 235, 0.08); + color: #2563eb; +} + +.dark .page-header__nav a:hover { + background: rgba(37, 99, 235, 0.15); + color: #60a5fa; +} + +.page-header__nav .back-link { + border: 1px solid #d1d5db; + color: #374151; +} + +.dark .page-header__nav .back-link { + border-color: var(--border-color); + color: var(--text-secondary); +} + +.page-header__nav .back-link:hover { + border-color: #2563eb; + color: #2563eb; + background: rgba(37, 99, 235, 0.06); +} + +.dark .page-header__nav .back-link:hover { + border-color: #60a5fa; + color: #60a5fa; + background: rgba(37, 99, 235, 0.12); +} + .page-header h1 { font-size: 2.4rem; margin: 0 0 0.5rem; color: #1d2736; } +.dark .page-header h1 { + color: var(--text-primary); +} + .page-header p { margin: 0; color: #5a6575; } +.dark .page-header p { + color: var(--text-secondary); +} + +.page-header-content { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 2rem; +} + +.page-header-title { + flex: 1; +} + +.dark-mode-toggle { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: rgba(59, 130, 246, 0.1); + border: 2px solid #2563eb; + border-radius: 999px; + color: #2563eb; + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.9rem; + font-weight: 500; +} + +.dark .dark-mode-toggle { + background: rgba(59, 130, 246, 0.15); + border-color: #60a5fa; + color: #60a5fa; +} + +.dark-mode-toggle:hover { + background: rgba(59, 130, 246, 0.15); + transform: translateY(-1px); +} + +.dark .dark-mode-toggle:hover { + background: rgba(59, 130, 246, 0.2); +} + .dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); @@ -47,6 +167,12 @@ body { border-radius: 16px; padding: 20px; box-shadow: 0 12px 24px rgba(18, 24, 32, 0.08); + transition: background-color 0.3s ease; +} + +.dark .stat-card { + background: var(--bg-card); + box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3); } .stat-card h2 { @@ -55,11 +181,19 @@ body { color: #64748b; } +.dark .stat-card h2 { + color: var(--text-muted); +} + .stat-card strong { font-size: 1.8rem; color: #1d2736; } +.dark .stat-card strong { + color: var(--text-primary); +} + .stat-card span { display: block; margin-top: 6px; @@ -67,9 +201,8 @@ body { font-size: 0.9rem; } -.panels { - display: grid; - gap: 24px; +.dark .stat-card span { + color: var(--text-secondary); } .panel { @@ -77,6 +210,12 @@ body { border-radius: 16px; padding: 24px; box-shadow: 0 10px 20px rgba(15, 23, 42, 0.08); + transition: background-color 0.3s ease; +} + +.dark .panel { + background: var(--bg-card); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3); } .panel h2 { @@ -85,6 +224,10 @@ body { color: #1d2736; } +.dark .panel h2 { + color: var(--text-primary); +} + .offers-table { width: 100%; border-collapse: collapse; @@ -97,6 +240,11 @@ body { border-bottom: 1px solid #e5e9f1; } +.dark .offers-table th, +.dark .offers-table td { + border-bottom-color: var(--border-color); +} + .offers-table th { color: #64748b; font-size: 0.85rem; @@ -104,18 +252,16 @@ body { letter-spacing: 0.05em; } +.dark .offers-table th { + color: var(--text-muted); +} + .offers-table tbody tr:hover { background-color: rgba(99, 102, 241, 0.08); } -.table-responsive { - overflow-x: auto; -} - -.offers-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: 20px; +.dark .offers-table tbody tr:hover { + background-color: var(--hover-bg); } .offer-card { @@ -126,30 +272,36 @@ body { transition: box-shadow 200ms ease, transform 200ms ease; } +.dark .offer-card { + background: var(--bg-card); + border-color: var(--border-color); +} + .offer-card:hover { box-shadow: 0 8px 24px rgba(99, 102, 241, 0.12); transform: translateY(-2px); } -.offer-card-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 4px; -} - .offer-gpu-name { font-size: 1.15rem; font-weight: 700; color: #1d2736; } +.dark .offer-gpu-name { + color: var(--text-primary); +} + .offer-provider { font-size: 0.85rem; color: #64748b; margin-bottom: 16px; } +.dark .offer-provider { + color: var(--text-secondary); +} + .offer-specs { display: grid; grid-template-columns: repeat(4, 1fr); @@ -160,8 +312,8 @@ body { margin-bottom: 16px; } -.spec-item { - text-align: center; +.dark .offer-specs { + background: var(--bg-secondary); } .spec-label { @@ -173,6 +325,10 @@ body { margin-bottom: 4px; } +.dark .spec-label { + color: var(--text-muted); +} + .spec-value { display: block; font-size: 0.95rem; @@ -180,10 +336,8 @@ body { color: #1e293b; } -.offer-pricing { - display: flex; - justify-content: space-between; - align-items: baseline; +.dark .spec-value { + color: var(--text-primary); } .offer-price { @@ -192,52 +346,27 @@ body { color: #6366f1; } +.dark .offer-price { + color: #a78bfa; +} + .offer-price small { font-size: 0.75rem; font-weight: 500; color: #94a3b8; } +.dark .offer-price small { + color: var(--text-muted); +} + .offer-sla { font-size: 0.8rem; color: #64748b; } -.offer-plugins { - margin-bottom: 8px; -} - -.plugin-badge { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 3px 10px; - border-radius: 999px; - font-size: 0.75rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.04em; - background: linear-gradient(135deg, #6366f1, #8b5cf6); - color: #ffffff; -} - -.offer-models { - margin-bottom: 16px; -} - -.models-label { - display: block; - font-size: 0.7rem; - text-transform: uppercase; - letter-spacing: 0.06em; - color: #94a3b8; - margin-bottom: 8px; -} - -.model-tags { - display: flex; - flex-wrap: wrap; - gap: 6px; +.dark .offer-sla { + color: var(--text-secondary); } .model-tag { @@ -251,6 +380,12 @@ body { border: 1px solid #e2e8f0; } +.dark .model-tag { + background: var(--bg-secondary); + color: var(--text-primary); + border-color: var(--border-color); +} + .status-pill { display: inline-flex; align-items: center; @@ -271,6 +406,11 @@ body { color: #1d4ed8; } +.dark .status-reserved { + background-color: rgba(59, 130, 246, 0.2); + color: #1d4ed8; +} + .bid-form { display: grid; gap: 16px; @@ -283,6 +423,10 @@ body { margin-bottom: 6px; } +.dark .bid-form label { + color: var(--text-primary); +} + .bid-form input, .bid-form select, .bid-form textarea { @@ -295,6 +439,14 @@ body { background-color: #f9fbff; } +.dark .bid-form input, +.dark .bid-form select, +.dark .bid-form textarea { + border-color: var(--border-color); + background-color: var(--bg-secondary); + color: var(--text-primary); +} + .bid-form button { justify-self: flex-start; background: linear-gradient(135deg, #6366f1, #8b5cf6); @@ -322,6 +474,12 @@ body { background-color: rgba(99, 102, 241, 0.05); } +.dark .empty-state { + color: var(--text-secondary); + border-color: var(--border-color); + background-color: rgba(99, 102, 241, 0.1); +} + .toast { position: fixed; bottom: 24px; @@ -336,6 +494,11 @@ body { transition: opacity 200ms ease, transform 200ms ease; } +.dark .toast { + background: var(--bg-card); + color: var(--text-primary); +} + .toast.visible { opacity: 1; transform: translateY(0); diff --git a/apps/trade-exchange/index.html b/apps/trade-exchange/index.html index ca8996a4..1b2937a9 100644 --- a/apps/trade-exchange/index.html +++ b/apps/trade-exchange/index.html @@ -58,6 +58,9 @@

AITBC Trade Exchange

@@ -641,7 +641,7 @@ gosec ./...
  • Documentation: Full Documentation
  • Community: Discord
  • Issues: GitHub Issues
  • -
  • Email: aitbc@bubuit.net
  • +
  • Email: andreas.fleckl@bubuit.net
  • diff --git a/website/docs/index.html b/website/docs/index.html index 64d3e9f6..e933da78 100644 --- a/website/docs/index.html +++ b/website/docs/index.html @@ -3,9 +3,12 @@ - + + + + + Documentation - AITBC - @@ -172,11 +175,7 @@ Source Code - - - Community - - + Support @@ -186,17 +185,11 @@

    Need Help?

    -

    Can't find what you're looking for? Our community is here to help!

    +

    Can't find what you're looking for? Our support team is here to help!

    diff --git a/website/docs/marketplace-web.html b/website/docs/marketplace-web.html index b90f8674..ddb46ace 100644 --- a/website/docs/marketplace-web.html +++ b/website/docs/marketplace-web.html @@ -260,7 +260,7 @@ docker run -d \ diff --git a/website/docs/miners.html b/website/docs/miners.html index 79dc7e64..bdef8cbb 100644 --- a/website/docs/miners.html +++ b/website/docs/miners.html @@ -315,7 +315,7 @@ nano ~/.aitbc/miner.toml
  • Check the logs: ./aitbc-miner logs
  • Visit our Discord community
  • Search issues on Gitea
  • -
  • Email support: aitbc@bubuit.net
  • +
  • Email support: andreas.fleckl@bubuit.net
  • diff --git a/website/docs/trade-exchange.html b/website/docs/trade-exchange.html index cb7a8e12..f260e6a1 100644 --- a/website/docs/trade-exchange.html +++ b/website/docs/trade-exchange.html @@ -260,7 +260,7 @@ BITCOIN_RPC_PASS=password

    Contact

    diff --git a/website/index.html b/website/index.html index 52a9b5a7..20a84f42 100644 --- a/website/index.html +++ b/website/index.html @@ -8,526 +8,23 @@ - - - + + + + + @@ -539,8 +36,10 @@

    AITBC