refactor(theme): remove light theme and enforce dark mode across all apps

This commit is contained in:
oib
2026-02-27 14:00:33 +01:00
parent 521f7ec04a
commit d023654e74
15 changed files with 89 additions and 447 deletions

View File

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

View File

@@ -1,23 +1,19 @@
from __future__ import annotations
import asyncio
import hashlib
import time
from dataclasses import dataclass
from datetime import datetime
import re
from datetime import datetime
from typing import Callable, ContextManager, Optional
import httpx
from sqlmodel import Session, select
from ..logger import get_logger
from ..metrics import metrics_registry
from ..models import Block, Transaction
from ..config import ProposerConfig
from ..models import Block
from ..gossip import gossip_broker
from ..mempool import get_mempool
_METRIC_KEY_SANITIZE = re.compile(r"[^0-9a-zA-Z]+")
_METRIC_KEY_SANITIZE = re.compile(r"[^a-zA-Z0-9_]")
def _sanitize_metric_suffix(value: str) -> str:
@@ -25,61 +21,19 @@ def _sanitize_metric_suffix(value: str) -> str:
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:
"""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__(
self,
*,
config: ProposerConfig,
session_factory: Callable[[], ContextManager[Session]],
circuit_breaker: Optional[CircuitBreaker] = None,
) -> None:
self._config = config
self._session_factory = session_factory
@@ -87,7 +41,6 @@ class PoAProposer:
self._stop_event = asyncio.Event()
self._task: Optional[asyncio.Task[None]] = None
self._last_proposer_id: Optional[str] = None
self._circuit_breaker = circuit_breaker or CircuitBreaker()
async def start(self) -> 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._ensure_genesis_block()
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:
if self._task is None:
@@ -105,31 +58,15 @@ class PoAProposer:
await self._task
self._task = None
@property
def is_healthy(self) -> bool:
return self._circuit_breaker.state != "open"
async def _run_loop(self) -> None:
metrics_registry.set_gauge("poa_proposer_running", 1.0)
try:
while not self._stop_event.is_set():
await self._wait_until_next_slot()
if self._stop_event.is_set():
break
if not self._circuit_breaker.allow_request():
self._logger.warning("Circuit breaker open, skipping block proposal")
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()
except Exception as exc: # pragma: no cover - defensive logging
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:
head = self._fetch_chain_head()
@@ -146,7 +83,24 @@ class PoAProposer:
return
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:
head = session.exec(select(Block).order_by(Block.height.desc()).limit(1)).first()
next_height = 0
@@ -157,13 +111,6 @@ class PoAProposer:
parent_hash = head.hash
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()
block_hash = self._compute_block_hash(next_height, parent_hash, timestamp)
@@ -173,33 +120,14 @@ class PoAProposer:
parent_hash=parent_hash,
proposer=self._config.proposer_id,
timestamp=timestamp,
tx_count=len(pending_txs),
tx_count=0,
state_root=None,
)
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()
# Metrics
build_duration = time.perf_counter() - start_time
metrics_registry.increment("blocks_proposed_total")
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:
metrics_registry.observe("block_interval_seconds", interval_seconds)
metrics_registry.set_gauge("poa_last_block_interval_seconds", float(interval_seconds))
@@ -207,33 +135,30 @@ class PoAProposer:
proposer_suffix = _sanitize_metric_suffix(self._config.proposer_id)
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:
metrics_registry.increment("poa_proposer_rotations_total")
metrics_registry.increment("poa_proposer_switches_total")
self._last_proposer_id = self._config.proposer_id
asyncio.create_task(
self._logger.info(
"Proposed block",
extra={
"height": block.height,
"hash": block.hash,
"proposer": block.proposer,
},
)
# 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,
},
)
)
self._logger.info(
"Proposed block",
extra={
"height": next_height,
"hash": block_hash,
"parent_hash": parent_hash,
"timestamp": timestamp.isoformat(),
"tx_count": len(pending_txs),
"total_fees": total_fees,
"build_ms": round(build_duration * 1000, 2),
},
"state_root": block.state_root,
}
)
def _ensure_genesis_block(self) -> None:
@@ -243,44 +168,36 @@ class PoAProposer:
return
timestamp = datetime.utcnow()
genesis_hash = self._compute_block_hash(0, "0x00", timestamp)
block_hash = self._compute_block_hash(0, "0x00", timestamp)
genesis = Block(
height=0,
hash=genesis_hash,
hash=block_hash,
parent_hash="0x00",
proposer=self._config.proposer_id,
proposer="genesis",
timestamp=timestamp,
tx_count=0,
state_root=None,
)
session.add(genesis)
session.commit()
asyncio.create_task(
# Broadcast genesis block for initial sync
gossip_broker.publish(
"blocks",
{
"height": genesis.height,
"hash": genesis.hash,
"parent_hash": genesis.parent_hash,
"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]:
for attempt in range(3):
try:
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:
payload = f"{self._config.chain_id}|{height}|{parent_hash}|{timestamp.isoformat()}".encode()

View File

@@ -271,89 +271,3 @@ selectors.bidForm?.addEventListener('submit', async (event) => {
});
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;

View File

@@ -428,17 +428,9 @@
});
// 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) {
// Apply theme immediately
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
else {
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
const mainSiteTheme = localStorage.getItem('theme');
@@ -476,17 +463,7 @@
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');
}
});
}
}

View File

@@ -116,17 +116,10 @@ python3 -m aitbc_agent.agent start
## 🔮 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
- Multi-modal processing capabilities
- Advanced swarm intelligence
- Edge computing integration
- Quantum computing preparation
## 🤝 Contributing

View File

@@ -21,16 +21,6 @@ body.dark {
--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 {
height: 90px;
@@ -45,7 +35,7 @@ body.light {
}
.global-header__inner {
max-width: 1200px;
max-width: 1160px;
margin: 0 auto;
padding: 0 1.25rem;
height: 100%;
@@ -137,27 +127,10 @@ body.light {
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 {
max-width: 1200px;
max-width: 1160px;
margin: 0 auto;
padding: 0 1.25rem 0.75rem;
display: flex;

View File

@@ -1,15 +1,11 @@
(function () {
// Immediately restore theme on script load
const savedTheme = localStorage.getItem('theme') || localStorage.getItem('exchangeTheme');
if (savedTheme) {
if (savedTheme === 'dark') {
// Always enforce dark theme
document.documentElement.setAttribute('data-theme', 'dark');
document.documentElement.classList.add('dark');
} else {
document.documentElement.removeAttribute('data-theme');
document.documentElement.classList.remove('dark');
}
}
// Clean up any old user preferences
if (localStorage.getItem('theme')) localStorage.removeItem('theme');
if (localStorage.getItem('exchangeTheme')) localStorage.removeItem('exchangeTheme');
const NAV_ITEMS = [
{ key: 'home', label: 'Home', href: '/' },
@@ -19,8 +15,6 @@
{ key: 'docs', label: 'Docs', href: '/docs/index.html' },
];
const CTA = { label: 'Launch Marketplace', href: '/marketplace/' };
function determineActiveKey(pathname) {
if (pathname.startsWith('/explorer')) return 'explorer';
if (pathname.startsWith('/marketplace')) return 'marketplace';
@@ -48,81 +42,11 @@
</div>
</a>
<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>
</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() {
const activeKey = determineActiveKey(window.location.pathname);
const headerHTML = buildHeader(activeKey);
@@ -135,8 +59,6 @@
} else {
document.body.insertAdjacentHTML('afterbegin', headerHTML);
}
bindThemeToggle();
}
if (document.readyState === 'loading') {

View File

@@ -30,22 +30,6 @@
--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

View File

@@ -51,12 +51,6 @@
<a href="/Exchange/" class="global-nav__link">Exchange</a>
<a href="/docs/index.html" class="global-nav__link">Docs</a>
</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>
</header>