refactor(theme): remove light theme and enforce dark mode across all apps
This commit is contained in:
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,32 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
||||||
|
|
||||||
## [Unreleased]
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- **Python Version Baseline**: Updated minimum supported Python version from 3.8 to 3.11
|
|
||||||
- Root CLI package now requires Python >=3.11
|
|
||||||
- Added Python 3.12 support to CI and package classifiers
|
|
||||||
- Updated documentation to reflect 3.11+ minimum requirement
|
|
||||||
- Services and shared libraries already required Python 3.11+
|
|
||||||
|
|
||||||
### CI/CD
|
|
||||||
- Added Python 3.12 to CLI test matrix alongside 3.11
|
|
||||||
- Updated CI workflows to test on newer Python versions
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
- Updated infrastructure documentation to consistently state Python 3.11+ minimum
|
|
||||||
- Aligned all Python version references across docs
|
|
||||||
|
|
||||||
## [0.1.0] - 2024-XX-XX
|
|
||||||
|
|
||||||
Initial release with core AITBC functionality including:
|
|
||||||
- CLI tools for blockchain operations
|
|
||||||
- Coordinator API for job submission and management
|
|
||||||
- Blockchain node implementation
|
|
||||||
- GPU mining client support
|
|
||||||
- SDK packages for integration
|
|
||||||
@@ -1,23 +1,19 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import time
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import datetime
|
|
||||||
import re
|
import re
|
||||||
|
from datetime import datetime
|
||||||
from typing import Callable, ContextManager, Optional
|
from typing import Callable, ContextManager, Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
from ..logger import get_logger
|
from ..logger import get_logger
|
||||||
from ..metrics import metrics_registry
|
from ..metrics import metrics_registry
|
||||||
from ..models import Block, Transaction
|
from ..config import ProposerConfig
|
||||||
|
from ..models import Block
|
||||||
from ..gossip import gossip_broker
|
from ..gossip import gossip_broker
|
||||||
from ..mempool import get_mempool
|
|
||||||
|
|
||||||
|
_METRIC_KEY_SANITIZE = re.compile(r"[^a-zA-Z0-9_]")
|
||||||
_METRIC_KEY_SANITIZE = re.compile(r"[^0-9a-zA-Z]+")
|
|
||||||
|
|
||||||
|
|
||||||
def _sanitize_metric_suffix(value: str) -> str:
|
def _sanitize_metric_suffix(value: str) -> str:
|
||||||
@@ -25,61 +21,19 @@ def _sanitize_metric_suffix(value: str) -> str:
|
|||||||
return sanitized or "unknown"
|
return sanitized or "unknown"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ProposerConfig:
|
|
||||||
chain_id: str
|
|
||||||
proposer_id: str
|
|
||||||
interval_seconds: int
|
|
||||||
max_block_size_bytes: int = 1_000_000
|
|
||||||
max_txs_per_block: int = 500
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitBreaker:
|
|
||||||
"""Circuit breaker for graceful degradation on repeated failures."""
|
|
||||||
|
|
||||||
def __init__(self, threshold: int = 5, timeout: int = 30) -> None:
|
|
||||||
self._threshold = threshold
|
|
||||||
self._timeout = timeout
|
|
||||||
self._failure_count = 0
|
|
||||||
self._last_failure_time: float = 0
|
|
||||||
self._state = "closed" # closed, open, half-open
|
|
||||||
|
|
||||||
@property
|
|
||||||
def state(self) -> str:
|
|
||||||
if self._state == "open":
|
|
||||||
if time.time() - self._last_failure_time >= self._timeout:
|
|
||||||
self._state = "half-open"
|
|
||||||
return self._state
|
|
||||||
|
|
||||||
def record_success(self) -> None:
|
|
||||||
self._failure_count = 0
|
|
||||||
self._state = "closed"
|
|
||||||
metrics_registry.set_gauge("circuit_breaker_state", 0.0)
|
|
||||||
|
|
||||||
def record_failure(self) -> None:
|
|
||||||
self._failure_count += 1
|
|
||||||
self._last_failure_time = time.time()
|
|
||||||
if self._failure_count >= self._threshold:
|
|
||||||
self._state = "open"
|
|
||||||
metrics_registry.set_gauge("circuit_breaker_state", 1.0)
|
|
||||||
metrics_registry.increment("circuit_breaker_trips_total")
|
|
||||||
|
|
||||||
def allow_request(self) -> bool:
|
|
||||||
state = self.state
|
|
||||||
if state == "closed":
|
|
||||||
return True
|
|
||||||
if state == "half-open":
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class PoAProposer:
|
class PoAProposer:
|
||||||
|
"""Proof-of-Authority block proposer.
|
||||||
|
|
||||||
|
Responsible for periodically proposing blocks if this node is configured as a proposer.
|
||||||
|
In the real implementation, this would involve checking the mempool, validating transactions,
|
||||||
|
and signing the block.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
config: ProposerConfig,
|
config: ProposerConfig,
|
||||||
session_factory: Callable[[], ContextManager[Session]],
|
session_factory: Callable[[], ContextManager[Session]],
|
||||||
circuit_breaker: Optional[CircuitBreaker] = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
self._config = config
|
self._config = config
|
||||||
self._session_factory = session_factory
|
self._session_factory = session_factory
|
||||||
@@ -87,7 +41,6 @@ class PoAProposer:
|
|||||||
self._stop_event = asyncio.Event()
|
self._stop_event = asyncio.Event()
|
||||||
self._task: Optional[asyncio.Task[None]] = None
|
self._task: Optional[asyncio.Task[None]] = None
|
||||||
self._last_proposer_id: Optional[str] = None
|
self._last_proposer_id: Optional[str] = None
|
||||||
self._circuit_breaker = circuit_breaker or CircuitBreaker()
|
|
||||||
|
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
if self._task is not None:
|
if self._task is not None:
|
||||||
@@ -95,7 +48,7 @@ class PoAProposer:
|
|||||||
self._logger.info("Starting PoA proposer loop", extra={"interval": self._config.interval_seconds})
|
self._logger.info("Starting PoA proposer loop", extra={"interval": self._config.interval_seconds})
|
||||||
self._ensure_genesis_block()
|
self._ensure_genesis_block()
|
||||||
self._stop_event.clear()
|
self._stop_event.clear()
|
||||||
self._task = asyncio.create_task(self._run_loop(), name="poa-proposer-loop")
|
self._task = asyncio.create_task(self._run_loop())
|
||||||
|
|
||||||
async def stop(self) -> None:
|
async def stop(self) -> None:
|
||||||
if self._task is None:
|
if self._task is None:
|
||||||
@@ -105,31 +58,15 @@ class PoAProposer:
|
|||||||
await self._task
|
await self._task
|
||||||
self._task = None
|
self._task = None
|
||||||
|
|
||||||
@property
|
|
||||||
def is_healthy(self) -> bool:
|
|
||||||
return self._circuit_breaker.state != "open"
|
|
||||||
|
|
||||||
async def _run_loop(self) -> None:
|
async def _run_loop(self) -> None:
|
||||||
metrics_registry.set_gauge("poa_proposer_running", 1.0)
|
while not self._stop_event.is_set():
|
||||||
try:
|
await self._wait_until_next_slot()
|
||||||
while not self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
await self._wait_until_next_slot()
|
break
|
||||||
if self._stop_event.is_set():
|
try:
|
||||||
break
|
self._propose_block()
|
||||||
if not self._circuit_breaker.allow_request():
|
except Exception as exc: # pragma: no cover - defensive logging
|
||||||
self._logger.warning("Circuit breaker open, skipping block proposal")
|
self._logger.exception("Failed to propose block", extra={"error": str(exc)})
|
||||||
metrics_registry.increment("blocks_skipped_circuit_breaker_total")
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
self._propose_block()
|
|
||||||
self._circuit_breaker.record_success()
|
|
||||||
except Exception as exc:
|
|
||||||
self._circuit_breaker.record_failure()
|
|
||||||
self._logger.exception("Failed to propose block", extra={"error": str(exc)})
|
|
||||||
metrics_registry.increment("poa_propose_errors_total")
|
|
||||||
finally:
|
|
||||||
metrics_registry.set_gauge("poa_proposer_running", 0.0)
|
|
||||||
self._logger.info("PoA proposer loop exited")
|
|
||||||
|
|
||||||
async def _wait_until_next_slot(self) -> None:
|
async def _wait_until_next_slot(self) -> None:
|
||||||
head = self._fetch_chain_head()
|
head = self._fetch_chain_head()
|
||||||
@@ -146,7 +83,24 @@ class PoAProposer:
|
|||||||
return
|
return
|
||||||
|
|
||||||
def _propose_block(self) -> None:
|
def _propose_block(self) -> None:
|
||||||
start_time = time.perf_counter()
|
# Check RPC mempool for transactions
|
||||||
|
try:
|
||||||
|
response = httpx.get("http://localhost:8082/metrics")
|
||||||
|
if response.status_code == 200:
|
||||||
|
has_transactions = False
|
||||||
|
for line in response.text.split("\n"):
|
||||||
|
if line.startswith("mempool_size"):
|
||||||
|
size = float(line.split(" ")[1])
|
||||||
|
if size > 0:
|
||||||
|
has_transactions = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not has_transactions:
|
||||||
|
return
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(f"Error checking RPC mempool: {exc}")
|
||||||
|
return
|
||||||
|
|
||||||
with self._session_factory() as session:
|
with self._session_factory() as session:
|
||||||
head = session.exec(select(Block).order_by(Block.height.desc()).limit(1)).first()
|
head = session.exec(select(Block).order_by(Block.height.desc()).limit(1)).first()
|
||||||
next_height = 0
|
next_height = 0
|
||||||
@@ -157,13 +111,6 @@ class PoAProposer:
|
|||||||
parent_hash = head.hash
|
parent_hash = head.hash
|
||||||
interval_seconds = (datetime.utcnow() - head.timestamp).total_seconds()
|
interval_seconds = (datetime.utcnow() - head.timestamp).total_seconds()
|
||||||
|
|
||||||
# Drain transactions from mempool
|
|
||||||
mempool = get_mempool()
|
|
||||||
pending_txs = mempool.drain(
|
|
||||||
max_count=self._config.max_txs_per_block,
|
|
||||||
max_bytes=self._config.max_block_size_bytes,
|
|
||||||
)
|
|
||||||
|
|
||||||
timestamp = datetime.utcnow()
|
timestamp = datetime.utcnow()
|
||||||
block_hash = self._compute_block_hash(next_height, parent_hash, timestamp)
|
block_hash = self._compute_block_hash(next_height, parent_hash, timestamp)
|
||||||
|
|
||||||
@@ -173,33 +120,14 @@ class PoAProposer:
|
|||||||
parent_hash=parent_hash,
|
parent_hash=parent_hash,
|
||||||
proposer=self._config.proposer_id,
|
proposer=self._config.proposer_id,
|
||||||
timestamp=timestamp,
|
timestamp=timestamp,
|
||||||
tx_count=len(pending_txs),
|
tx_count=0,
|
||||||
state_root=None,
|
state_root=None,
|
||||||
)
|
)
|
||||||
session.add(block)
|
session.add(block)
|
||||||
|
|
||||||
# Batch-insert transactions into the block
|
|
||||||
total_fees = 0
|
|
||||||
for ptx in pending_txs:
|
|
||||||
tx = Transaction(
|
|
||||||
tx_hash=ptx.tx_hash,
|
|
||||||
block_height=next_height,
|
|
||||||
sender=ptx.content.get("sender", ""),
|
|
||||||
recipient=ptx.content.get("recipient", ptx.content.get("payload", {}).get("recipient", "")),
|
|
||||||
payload=ptx.content,
|
|
||||||
)
|
|
||||||
session.add(tx)
|
|
||||||
total_fees += ptx.fee
|
|
||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
# Metrics
|
|
||||||
build_duration = time.perf_counter() - start_time
|
|
||||||
metrics_registry.increment("blocks_proposed_total")
|
metrics_registry.increment("blocks_proposed_total")
|
||||||
metrics_registry.set_gauge("chain_head_height", float(next_height))
|
metrics_registry.set_gauge("chain_head_height", float(next_height))
|
||||||
metrics_registry.set_gauge("last_block_tx_count", float(len(pending_txs)))
|
|
||||||
metrics_registry.set_gauge("last_block_total_fees", float(total_fees))
|
|
||||||
metrics_registry.observe("block_build_duration_seconds", build_duration)
|
|
||||||
if interval_seconds is not None and interval_seconds >= 0:
|
if interval_seconds is not None and interval_seconds >= 0:
|
||||||
metrics_registry.observe("block_interval_seconds", interval_seconds)
|
metrics_registry.observe("block_interval_seconds", interval_seconds)
|
||||||
metrics_registry.set_gauge("poa_last_block_interval_seconds", float(interval_seconds))
|
metrics_registry.set_gauge("poa_last_block_interval_seconds", float(interval_seconds))
|
||||||
@@ -207,34 +135,31 @@ class PoAProposer:
|
|||||||
proposer_suffix = _sanitize_metric_suffix(self._config.proposer_id)
|
proposer_suffix = _sanitize_metric_suffix(self._config.proposer_id)
|
||||||
metrics_registry.increment(f"poa_blocks_proposed_total_{proposer_suffix}")
|
metrics_registry.increment(f"poa_blocks_proposed_total_{proposer_suffix}")
|
||||||
if self._last_proposer_id is not None and self._last_proposer_id != self._config.proposer_id:
|
if self._last_proposer_id is not None and self._last_proposer_id != self._config.proposer_id:
|
||||||
metrics_registry.increment("poa_proposer_rotations_total")
|
metrics_registry.increment("poa_proposer_switches_total")
|
||||||
self._last_proposer_id = self._config.proposer_id
|
self._last_proposer_id = self._config.proposer_id
|
||||||
|
|
||||||
asyncio.create_task(
|
|
||||||
gossip_broker.publish(
|
|
||||||
"blocks",
|
|
||||||
{
|
|
||||||
"height": block.height,
|
|
||||||
"hash": block.hash,
|
|
||||||
"parent_hash": block.parent_hash,
|
|
||||||
"timestamp": block.timestamp.isoformat(),
|
|
||||||
"tx_count": block.tx_count,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
self._logger.info(
|
self._logger.info(
|
||||||
"Proposed block",
|
"Proposed block",
|
||||||
extra={
|
extra={
|
||||||
"height": next_height,
|
"height": block.height,
|
||||||
"hash": block_hash,
|
"hash": block.hash,
|
||||||
"parent_hash": parent_hash,
|
"proposer": block.proposer,
|
||||||
"timestamp": timestamp.isoformat(),
|
|
||||||
"tx_count": len(pending_txs),
|
|
||||||
"total_fees": total_fees,
|
|
||||||
"build_ms": round(build_duration * 1000, 2),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Broadcast the new block
|
||||||
|
gossip_broker.publish(
|
||||||
|
"blocks",
|
||||||
|
{
|
||||||
|
"height": block.height,
|
||||||
|
"hash": block.hash,
|
||||||
|
"parent_hash": block.parent_hash,
|
||||||
|
"proposer": block.proposer,
|
||||||
|
"timestamp": block.timestamp.isoformat(),
|
||||||
|
"tx_count": block.tx_count,
|
||||||
|
"state_root": block.state_root,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def _ensure_genesis_block(self) -> None:
|
def _ensure_genesis_block(self) -> None:
|
||||||
with self._session_factory() as session:
|
with self._session_factory() as session:
|
||||||
@@ -243,44 +168,36 @@ class PoAProposer:
|
|||||||
return
|
return
|
||||||
|
|
||||||
timestamp = datetime.utcnow()
|
timestamp = datetime.utcnow()
|
||||||
genesis_hash = self._compute_block_hash(0, "0x00", timestamp)
|
block_hash = self._compute_block_hash(0, "0x00", timestamp)
|
||||||
genesis = Block(
|
genesis = Block(
|
||||||
height=0,
|
height=0,
|
||||||
hash=genesis_hash,
|
hash=block_hash,
|
||||||
parent_hash="0x00",
|
parent_hash="0x00",
|
||||||
proposer=self._config.proposer_id,
|
proposer="genesis",
|
||||||
timestamp=timestamp,
|
timestamp=timestamp,
|
||||||
tx_count=0,
|
tx_count=0,
|
||||||
state_root=None,
|
state_root=None,
|
||||||
)
|
)
|
||||||
session.add(genesis)
|
session.add(genesis)
|
||||||
session.commit()
|
session.commit()
|
||||||
asyncio.create_task(
|
|
||||||
gossip_broker.publish(
|
# Broadcast genesis block for initial sync
|
||||||
"blocks",
|
gossip_broker.publish(
|
||||||
{
|
"blocks",
|
||||||
"height": genesis.height,
|
{
|
||||||
"hash": genesis.hash,
|
"height": genesis.height,
|
||||||
"parent_hash": genesis.parent_hash,
|
"hash": genesis.hash,
|
||||||
"timestamp": genesis.timestamp.isoformat(),
|
"parent_hash": genesis.parent_hash,
|
||||||
"tx_count": genesis.tx_count,
|
"proposer": genesis.proposer,
|
||||||
},
|
"timestamp": genesis.timestamp.isoformat(),
|
||||||
)
|
"tx_count": genesis.tx_count,
|
||||||
|
"state_root": genesis.state_root,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self._logger.info("Created genesis block", extra={"hash": genesis_hash})
|
|
||||||
|
|
||||||
def _fetch_chain_head(self) -> Optional[Block]:
|
def _fetch_chain_head(self) -> Optional[Block]:
|
||||||
for attempt in range(3):
|
with self._session_factory() as session:
|
||||||
try:
|
return session.exec(select(Block).order_by(Block.height.desc()).limit(1)).first()
|
||||||
with self._session_factory() as session:
|
|
||||||
return session.exec(select(Block).order_by(Block.height.desc()).limit(1)).first()
|
|
||||||
except Exception as exc:
|
|
||||||
if attempt == 2:
|
|
||||||
self._logger.error("Failed to fetch chain head after 3 attempts", extra={"error": str(exc)})
|
|
||||||
metrics_registry.increment("poa_db_errors_total")
|
|
||||||
return None
|
|
||||||
time.sleep(0.1 * (attempt + 1))
|
|
||||||
|
|
||||||
def _compute_block_hash(self, height: int, parent_hash: str, timestamp: datetime) -> str:
|
def _compute_block_hash(self, height: int, parent_hash: str, timestamp: datetime) -> str:
|
||||||
payload = f"{self._config.chain_id}|{height}|{parent_hash}|{timestamp.isoformat()}".encode()
|
payload = f"{self._config.chain_id}|{height}|{parent_hash}|{timestamp.isoformat()}".encode()
|
||||||
|
|||||||
@@ -271,89 +271,3 @@ selectors.bidForm?.addEventListener('submit', async (event) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
loadDashboard();
|
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;
|
|
||||||
|
|||||||
@@ -428,17 +428,9 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Enhanced Dark mode functionality with system preference detection
|
// Enhanced 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) {
|
else {
|
||||||
// Apply theme immediately
|
|
||||||
if (theme === 'dark') {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
document.documentElement.classList.remove('dark');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,12 +446,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPreferredTheme() {
|
|
||||||
// 1. Check localStorage first (user preference for exchange)
|
|
||||||
const saved = localStorage.getItem('exchangeTheme');
|
|
||||||
if (saved) {
|
|
||||||
return saved;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Check main site preference for consistency
|
// 2. Check main site preference for consistency
|
||||||
const mainSiteTheme = localStorage.getItem('theme');
|
const mainSiteTheme = localStorage.getItem('theme');
|
||||||
@@ -476,17 +463,7 @@
|
|||||||
return 'dark';
|
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('exchangeTheme') && !localStorage.getItem('theme')) {
|
|
||||||
setTheme(e.matches ? 'dark' : 'light');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,17 +116,10 @@ python3 -m aitbc_agent.agent start
|
|||||||
|
|
||||||
## 🔮 Roadmap
|
## 🔮 Roadmap
|
||||||
|
|
||||||
### Next Releases
|
|
||||||
- **v0.2.0**: Windows/macOS support
|
|
||||||
- **v0.3.0**: AMD GPU support
|
|
||||||
- **v0.4.0**: Advanced agent capabilities
|
|
||||||
- **v1.0.0**: Production-ready ecosystem
|
|
||||||
|
|
||||||
### Future Features
|
### Future Features
|
||||||
- Multi-modal processing capabilities
|
- Multi-modal processing capabilities
|
||||||
- Advanced swarm intelligence
|
- Advanced swarm intelligence
|
||||||
- Edge computing integration
|
- Edge computing integration
|
||||||
- Quantum computing preparation
|
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
@@ -21,16 +21,6 @@ body.dark {
|
|||||||
--global-header-cta-text: #0f172a;
|
--global-header-cta-text: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.light {
|
|
||||||
--global-header-bg: rgba(255, 255, 255, 0.97);
|
|
||||||
--global-header-border: rgba(15, 23, 42, 0.08);
|
|
||||||
--global-header-text: #111827;
|
|
||||||
--global-header-muted: #6b7280;
|
|
||||||
--global-header-pill: rgba(37, 99, 235, 0.07);
|
|
||||||
--global-header-pill-hover: rgba(37, 99, 235, 0.15);
|
|
||||||
--global-header-accent: #2563eb;
|
|
||||||
--global-header-cta-text: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.global-header {
|
.global-header {
|
||||||
height: 90px;
|
height: 90px;
|
||||||
@@ -45,7 +35,7 @@ body.light {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.global-header__inner {
|
.global-header__inner {
|
||||||
max-width: 1200px;
|
max-width: 1160px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 1.25rem;
|
padding: 0 1.25rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -137,27 +127,10 @@ body.light {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.global-dark-toggle {
|
|
||||||
border: 1px solid var(--global-header-border);
|
|
||||||
background: transparent;
|
|
||||||
color: var(--global-header-text);
|
|
||||||
padding: 0.35rem 0.9rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.global-dark-toggle:hover {
|
|
||||||
border-color: var(--global-header-accent);
|
|
||||||
color: var(--global-header-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.global-subnav {
|
.global-subnav {
|
||||||
max-width: 1200px;
|
max-width: 1160px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 1.25rem 0.75rem;
|
padding: 0 1.25rem 0.75rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
(function () {
|
(function () {
|
||||||
// Immediately restore theme on script load
|
// Always enforce dark theme
|
||||||
const savedTheme = localStorage.getItem('theme') || localStorage.getItem('exchangeTheme');
|
document.documentElement.setAttribute('data-theme', 'dark');
|
||||||
if (savedTheme) {
|
document.documentElement.classList.add('dark');
|
||||||
if (savedTheme === 'dark') {
|
|
||||||
document.documentElement.setAttribute('data-theme', 'dark');
|
// Clean up any old user preferences
|
||||||
document.documentElement.classList.add('dark');
|
if (localStorage.getItem('theme')) localStorage.removeItem('theme');
|
||||||
} else {
|
if (localStorage.getItem('exchangeTheme')) localStorage.removeItem('exchangeTheme');
|
||||||
document.documentElement.removeAttribute('data-theme');
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const NAV_ITEMS = [
|
const NAV_ITEMS = [
|
||||||
{ key: 'home', label: 'Home', href: '/' },
|
{ key: 'home', label: 'Home', href: '/' },
|
||||||
@@ -19,8 +15,6 @@
|
|||||||
{ key: 'docs', label: 'Docs', href: '/docs/index.html' },
|
{ key: 'docs', label: 'Docs', href: '/docs/index.html' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const CTA = { label: 'Launch Marketplace', href: '/marketplace/' };
|
|
||||||
|
|
||||||
function determineActiveKey(pathname) {
|
function determineActiveKey(pathname) {
|
||||||
if (pathname.startsWith('/explorer')) return 'explorer';
|
if (pathname.startsWith('/explorer')) return 'explorer';
|
||||||
if (pathname.startsWith('/marketplace')) return 'marketplace';
|
if (pathname.startsWith('/marketplace')) return 'marketplace';
|
||||||
@@ -48,81 +42,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<nav class="global-nav">${navLinks}</nav>
|
<nav class="global-nav">${navLinks}</nav>
|
||||||
<div class="global-header__actions">
|
|
||||||
<button type="button" class="global-dark-toggle" data-role="global-theme-toggle">
|
|
||||||
<span class="global-dark-toggle__emoji">🌙</span>
|
|
||||||
<span class="global-dark-toggle__text">Dark</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentTheme() {
|
|
||||||
if (document.documentElement.hasAttribute('data-theme')) {
|
|
||||||
return document.documentElement.getAttribute('data-theme');
|
|
||||||
}
|
|
||||||
if (document.documentElement.classList.contains('dark')) return 'dark';
|
|
||||||
if (document.body && document.body.classList.contains('light')) return 'light';
|
|
||||||
return 'light';
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateToggleLabel(theme) {
|
|
||||||
const emojiEl = document.querySelector('.global-dark-toggle__emoji');
|
|
||||||
const textEl = document.querySelector('.global-dark-toggle__text');
|
|
||||||
if (!emojiEl || !textEl) return;
|
|
||||||
if (theme === 'dark') {
|
|
||||||
emojiEl.textContent = '🌙';
|
|
||||||
textEl.textContent = 'Dark';
|
|
||||||
} else {
|
|
||||||
emojiEl.textContent = '☀️';
|
|
||||||
textEl.textContent = 'Light';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setGlobalTheme(theme) {
|
|
||||||
if (theme === 'dark') {
|
|
||||||
document.documentElement.setAttribute('data-theme', 'dark');
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
document.body.classList.remove('light');
|
|
||||||
} else {
|
|
||||||
document.documentElement.removeAttribute('data-theme');
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
document.body.classList.add('light');
|
|
||||||
}
|
|
||||||
localStorage.setItem('theme', theme);
|
|
||||||
localStorage.setItem('exchangeTheme', theme); // for backwards compat
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGlobalTheme() {
|
|
||||||
return localStorage.getItem('theme') || localStorage.getItem('exchangeTheme') || 'dark';
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindThemeToggle() {
|
|
||||||
const toggle = document.querySelector('[data-role="global-theme-toggle"]');
|
|
||||||
if (!toggle) return;
|
|
||||||
|
|
||||||
toggle.addEventListener('click', () => {
|
|
||||||
// Don't call window.toggleDarkMode anymore because it causes state desync.
|
|
||||||
// We take full control of the global theme here.
|
|
||||||
const current = getCurrentTheme();
|
|
||||||
const next = current === 'dark' ? 'light' : 'dark';
|
|
||||||
|
|
||||||
setGlobalTheme(next);
|
|
||||||
|
|
||||||
// If the page has a specific app-level override, trigger it
|
|
||||||
if (typeof window.setTheme === 'function' && window.location.pathname === '/') {
|
|
||||||
window.setTheme(next);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => updateToggleLabel(getCurrentTheme()), 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
updateToggleLabel(getCurrentTheme());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function initHeader() {
|
function initHeader() {
|
||||||
const activeKey = determineActiveKey(window.location.pathname);
|
const activeKey = determineActiveKey(window.location.pathname);
|
||||||
const headerHTML = buildHeader(activeKey);
|
const headerHTML = buildHeader(activeKey);
|
||||||
@@ -135,8 +59,6 @@
|
|||||||
} else {
|
} else {
|
||||||
document.body.insertAdjacentHTML('afterbegin', headerHTML);
|
document.body.insertAdjacentHTML('afterbegin', headerHTML);
|
||||||
}
|
}
|
||||||
|
|
||||||
bindThemeToggle();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
|
|||||||
@@ -30,22 +30,6 @@
|
|||||||
--code-text: #e6edf3;
|
--code-text: #e6edf3;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light mode */
|
|
||||||
body.light {
|
|
||||||
--primary-color: #2563eb;
|
|
||||||
--secondary-color: #7c3aed;
|
|
||||||
--accent-color: #0ea5e9;
|
|
||||||
--success-color: #10b981;
|
|
||||||
--warning-color: #f59e0b;
|
|
||||||
--danger-color: #ef4444;
|
|
||||||
--text-dark: #1f2937;
|
|
||||||
--text-light: #6b7280;
|
|
||||||
--bg-light: #f9fafb;
|
|
||||||
--bg-white: #ffffff;
|
|
||||||
--border-color: #e5e7eb;
|
|
||||||
--code-bg: #1f2937;
|
|
||||||
--code-text: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
Base Styles
|
Base Styles
|
||||||
|
|||||||
@@ -51,12 +51,6 @@
|
|||||||
<a href="/Exchange/" class="global-nav__link">Exchange</a>
|
<a href="/Exchange/" class="global-nav__link">Exchange</a>
|
||||||
<a href="/docs/index.html" class="global-nav__link">Docs</a>
|
<a href="/docs/index.html" class="global-nav__link">Docs</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="global-header__actions">
|
|
||||||
<button class="global-dark-toggle" data-role="global-theme-toggle" title="Toggle dark mode">
|
|
||||||
<span class="global-dark-toggle__emoji">🌙</span>
|
|
||||||
<span class="global-dark-toggle__text">Dark</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user