Merge docker-setup: Docker stack, Purple theme, chart improvements
- Docker Compose stack (explorer, sync-blocks, sync-peers, MongoDB) with entrypoint envsubst for settings.json generation at runtime - Purple theme rebuilt from scratch with WCAG-compliant contrast (4-level luminance scale, 16:1 body text, 8.5:1 card headers) - Professional chart axes: K/M/G/T/P number abbreviations, Inter font, dark-glass tooltips, reduced X-axis tick density - Hashrate auto-scaling: panel and chart always display 1-999 with correct unit (MH/s, GH/s, TH/s, PH/s) - Coin supply via TXOUTSET (BitcoinPurple has no getinfo RPC) - btcp.png as favicon and logo Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@@ -0,0 +1,12 @@
|
||||
node_modules/
|
||||
db/
|
||||
tmp/
|
||||
.git/
|
||||
.gitignore
|
||||
settings.json
|
||||
*.log
|
||||
explorerdb_dump.7z
|
||||
docker-compose.yml
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
@@ -0,0 +1,30 @@
|
||||
# ─── Coin ────────────────────────────────────────────────────────────────────
|
||||
COIN_NAME=BitcoinPurple
|
||||
COIN_SYMBOL=BTCP
|
||||
# POW, POS, or Hybrid
|
||||
COIN_DIFFICULTY=POW
|
||||
|
||||
# ─── MongoDB (internal to this stack) ────────────────────────────────────────
|
||||
MONGO_HOST=mongodb
|
||||
MONGO_PORT=27017
|
||||
MONGO_USER=eiquidus
|
||||
MONGO_PASSWORD=change-me
|
||||
MONGO_DATABASE=explorerdb
|
||||
|
||||
# ─── Wallet RPC ───────────────────────────────────────────────────────────────
|
||||
# bitcoinpurpled container is reachable on the shared 'purple' Docker network
|
||||
WALLET_RPC_HOST=bitcoinpurpled
|
||||
WALLET_RPC_PORT=13495
|
||||
# Copy rpcuser / rpcpassword values from ~/.bitcoinpurple/bitcoinpurple.conf
|
||||
WALLET_RPC_USER=
|
||||
WALLET_RPC_PASS=
|
||||
|
||||
# ─── Explorer web ────────────────────────────────────────────────────────────
|
||||
# Host port the explorer is published on
|
||||
EXPLORER_PORT=3001
|
||||
# Bootswatch theme: Cerulean, Cosmo, Cyborg, Darkly, Flatly, Slate, Solar, ...
|
||||
EXPLORER_THEME=Purple
|
||||
|
||||
# ─── Sync intervals (seconds) ────────────────────────────────────────────────
|
||||
SYNC_BLOCKS_INTERVAL=120
|
||||
SYNC_PEERS_INTERVAL=300
|
||||
@@ -10,6 +10,7 @@ logs
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
db/
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
@@ -29,9 +30,10 @@ build/Release
|
||||
node_modules
|
||||
|
||||
settings.json
|
||||
.env
|
||||
.idea
|
||||
*~
|
||||
|
||||
.DS_Store
|
||||
|
||||
package-lock.json
|
||||
package-lock.json
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**eIquidus** — a Node.js/Express blockchain block explorer for altcoins using the Bitcoin RPC protocol. It connects to a coin wallet via JSON-RPC, syncs blocks/transactions into MongoDB, and serves a web UI.
|
||||
|
||||
- Node.js ≥ 20.9.0, MongoDB ≥ 7.0.2
|
||||
- Express v5, Pug templates, Mongoose ODM, Jasmine tests
|
||||
- Configuration lives entirely in `settings.json` (created from `settings.json.template`)
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
# Run tests
|
||||
npm test # jasmine test/*Spec.js
|
||||
|
||||
# Start the explorer
|
||||
npm start # cluster mode (1 worker per CPU)
|
||||
npm run start-instance # single instance (dev/testing)
|
||||
npm run start-pm2 # production via PM2
|
||||
|
||||
# Blockchain sync (run as separate processes, not part of the web server)
|
||||
npm run sync-blocks # sync new blocks from wallet
|
||||
npm run sync-markets # fetch exchange market data
|
||||
npm run sync-peers # discover network peers
|
||||
npm run sync-masternodes # update masternode list
|
||||
npm run check-blocks # verify block integrity
|
||||
|
||||
# Database maintenance
|
||||
npm run reindex # full reindex from wallet RPC
|
||||
npm run create-backup # backup MongoDB
|
||||
npm run restore-backup # restore MongoDB
|
||||
npm run delete-database # wipe all explorer data
|
||||
|
||||
# CSS compilation (SASS → CSS in public/css/)
|
||||
node scripts/compile_css.js
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Request Flow
|
||||
|
||||
```
|
||||
HTTP request
|
||||
→ app.js (middleware: CORS, rate limiting, auth, TLS redirect)
|
||||
→ routes/index.js (all web & API routes)
|
||||
→ lib/database.js (Mongoose queries against models/)
|
||||
→ lib/explorer.js (data transformation utilities)
|
||||
→ views/*.pug (rendered response)
|
||||
```
|
||||
|
||||
For live wallet data, routes call `lib/nodeapi.js` which wraps `lib/jsonrpc.js` for RPC calls.
|
||||
|
||||
### Sync Flow
|
||||
|
||||
```
|
||||
npm run sync-blocks
|
||||
→ scripts/sync.js (orchestrator, uses worker threads)
|
||||
→ lib/block_sync.js (parallel block fetching & processing)
|
||||
→ lib/database.js (bulk writes to MongoDB)
|
||||
```
|
||||
|
||||
Block sync is multi-threaded (configurable parallelism). It has elastic stack-size handling for overflow errors. **The web server and sync scripts are separate processes** — sync is triggered via cron, not by the web server.
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `app.js` | Express setup: plugin loader, CORS, ACL builder, TLS redirect |
|
||||
| `lib/settings.js` | Parses and validates `settings.json`; all config access goes through here |
|
||||
| `lib/database.js` | All MongoDB queries; wraps Mongoose models |
|
||||
| `lib/explorer.js` | Pure utility functions for formatting block/tx/address data |
|
||||
| `lib/block_sync.js` | Multi-threaded block synchronization engine |
|
||||
| `lib/nodeapi.js` | Wallet RPC wrapper |
|
||||
| `routes/index.js` | All Express routes (web pages + REST API) |
|
||||
| `scripts/sync.js` | Entry point for `npm run sync-*` commands |
|
||||
| `models/` | 12 Mongoose schemas: `tx`, `address`, `addresstx`, `stats`, `richlist`, `masternode`, `peers`, `markets`, `orphans`, `heavy`, `networkhistory`, `claimaddress` |
|
||||
| `lib/markets/` | Per-exchange integrations (altmarkets, dextrade, dexomy, freiexchange, nonkyc, poloniex, xeggex, yobit) |
|
||||
|
||||
### Configuration
|
||||
|
||||
`settings.json` (gitignored, based on `settings.json.template`) controls everything: DB credentials, wallet RPC, port, TLS, themes, enabled pages, market exchanges, API access. Read through `lib/settings.js` to understand available options and their defaults.
|
||||
|
||||
### Plugin System
|
||||
|
||||
Early-stage plugin system. Plugins are loaded in `app.js` from the `plugins/` directory. Currently a placeholder only.
|
||||
|
||||
### Testing
|
||||
|
||||
Tests are in `test/` using Jasmine:
|
||||
- `explorerSpec.js` — tests for `lib/explorer.js` utilities
|
||||
- `marketSpec.js` — tests for market helper functions
|
||||
- `testData.js` — shared fixtures
|
||||
|
||||
There is no linting configured. No TypeScript.
|
||||
@@ -0,0 +1,21 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
# gettext provides envsubst for settings.json template rendering
|
||||
RUN apk add --no-cache gettext bash
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install --omit=dev
|
||||
|
||||
COPY . .
|
||||
|
||||
# Compile SCSS → CSS (prestart.js also connects to MongoDB which isn't available at build time)
|
||||
RUN node ./scripts/compile_css.js
|
||||
|
||||
RUN chmod +x /app/docker/entrypoint.sh && mkdir -p /app/tmp
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
||||
CMD ["web"]
|
||||
@@ -0,0 +1,102 @@
|
||||
networks:
|
||||
purple:
|
||||
external: true # shared with purple-stack (bitcoinpurpled, electrumx)
|
||||
explorer-internal: # isolates MongoDB from the outside world
|
||||
name: purple-explorer-internal
|
||||
|
||||
services:
|
||||
mongodb:
|
||||
image: mongo:7
|
||||
container_name: purple-explorer-mongodb
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- explorer-internal
|
||||
volumes:
|
||||
# Keep MongoDB data in this repository's ./db directory.
|
||||
# Do not switch this back to a named Docker volume or the explorer will
|
||||
# stop using the already-synced local database.
|
||||
- type: bind
|
||||
source: ./db
|
||||
target: /data/db
|
||||
bind:
|
||||
create_host_path: true
|
||||
- ./docker/mongo-init.sh:/docker-entrypoint-initdb.d/init.sh:ro
|
||||
environment:
|
||||
# Root admin (used only by the init script to bootstrap the app user)
|
||||
MONGO_INITDB_ROOT_USERNAME: mongoadmin
|
||||
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD}
|
||||
# Passed into mongo-init.sh to create the explorer app user
|
||||
MONGO_APP_USER: ${MONGO_USER}
|
||||
MONGO_APP_PASS: ${MONGO_PASSWORD}
|
||||
MONGO_APP_DB: ${MONGO_DATABASE:-explorerdb}
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
explorer:
|
||||
build: .
|
||||
image: purple-explorer:local
|
||||
container_name: purple-explorer
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- mongodb
|
||||
networks:
|
||||
- purple # reach bitcoinpurpled:13495 for RPC
|
||||
- explorer-internal
|
||||
ports:
|
||||
- "0.0.0.0:${EXPLORER_PORT:-3001}:3001"
|
||||
env_file: .env
|
||||
command: web
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
sync-blocks:
|
||||
image: purple-explorer:local
|
||||
container_name: purple-explorer-sync-blocks
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- mongodb
|
||||
- explorer # ensures the image is built before this runs
|
||||
networks:
|
||||
- purple # reach bitcoinpurpled RPC
|
||||
- explorer-internal
|
||||
env_file: .env
|
||||
command: sync-blocks
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# sync-markets: disabled until exchanges are configured in docker/settings.json.tmpl
|
||||
# To enable: uncomment, configure markets_page.exchanges in the template, then redeploy.
|
||||
# sync-markets:
|
||||
# image: purple-explorer:local
|
||||
# container_name: purple-explorer-sync-markets
|
||||
# restart: unless-stopped
|
||||
# depends_on: [mongodb]
|
||||
# networks: [explorer-internal]
|
||||
# env_file: .env
|
||||
# command: sync-markets
|
||||
|
||||
sync-peers:
|
||||
image: purple-explorer:local
|
||||
container_name: purple-explorer-sync-peers
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- mongodb
|
||||
networks:
|
||||
- purple # reach bitcoinpurpled for peer info
|
||||
- explorer-internal
|
||||
env_file: .env
|
||||
command: sync-peers
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Generate settings.json from template using environment variables
|
||||
envsubst < /app/docker/settings.json.tmpl > /app/settings.json
|
||||
|
||||
MODE="${1:-web}"
|
||||
|
||||
case "$MODE" in
|
||||
web)
|
||||
# Initialize DB collections/stats document before starting workers
|
||||
# (equivalent to what prestart.js does before launching pm2/forever)
|
||||
node -e "
|
||||
const db = require('./lib/database');
|
||||
db.connect(null, function() {
|
||||
db.initialize_data_startup(function() {
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
"
|
||||
exec node --stack-size=10000 ./bin/cluster
|
||||
;;
|
||||
|
||||
sync-blocks)
|
||||
echo "[sync-blocks] Starting block sync loop (interval: ${SYNC_BLOCKS_INTERVAL:-120}s)"
|
||||
while true; do
|
||||
node --stack-size=10000 ./scripts/sync.js index update || true
|
||||
sleep "${SYNC_BLOCKS_INTERVAL:-120}"
|
||||
done
|
||||
;;
|
||||
|
||||
sync-markets)
|
||||
echo "[sync-markets] Starting market sync (runs in internal loop)"
|
||||
exec node --stack-size=10000 ./scripts/sync.js market
|
||||
;;
|
||||
|
||||
sync-peers)
|
||||
echo "[sync-peers] Starting peer sync loop (interval: ${SYNC_PEERS_INTERVAL:-300}s)"
|
||||
while true; do
|
||||
node --stack-size=10000 ./scripts/sync.js peers || true
|
||||
sleep "${SYNC_PEERS_INTERVAL:-300}"
|
||||
done
|
||||
;;
|
||||
|
||||
reindex)
|
||||
echo "[reindex] Running full reindex..."
|
||||
exec node --stack-size=10000 ./scripts/sync.js index reindex
|
||||
;;
|
||||
|
||||
check-blocks)
|
||||
echo "[check-blocks] Checking for missing transactions..."
|
||||
exec node --stack-size=10000 ./scripts/sync.js index check
|
||||
;;
|
||||
|
||||
*)
|
||||
exec "$@"
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
# Creates the explorer app user in explorerdb.
|
||||
# Runs once on first container start (only when the data volume is empty).
|
||||
mongosh -u "$MONGO_INITDB_ROOT_USERNAME" -p "$MONGO_INITDB_ROOT_PASSWORD" --authenticationDatabase admin <<EOF
|
||||
use $MONGO_APP_DB
|
||||
db.createUser({
|
||||
user: "$MONGO_APP_USER",
|
||||
pwd: "$MONGO_APP_PASS",
|
||||
roles: [{ role: "readWrite", db: "$MONGO_APP_DB" }]
|
||||
})
|
||||
EOF
|
||||
@@ -0,0 +1,262 @@
|
||||
{
|
||||
"locale": "locale/en.json",
|
||||
|
||||
"dbsettings": {
|
||||
"user": "${MONGO_USER}",
|
||||
"password": "${MONGO_PASSWORD}",
|
||||
"database": "${MONGO_DATABASE}",
|
||||
"address": "${MONGO_HOST}",
|
||||
"port": "${MONGO_PORT}"
|
||||
},
|
||||
|
||||
"wallet": {
|
||||
"host": "${WALLET_RPC_HOST}",
|
||||
"port": "${WALLET_RPC_PORT}",
|
||||
"username": "${WALLET_RPC_USER}",
|
||||
"password": "${WALLET_RPC_PASS}"
|
||||
},
|
||||
|
||||
"webserver": {
|
||||
"port": 3001,
|
||||
"tls": {
|
||||
"enabled": false,
|
||||
"port": 443,
|
||||
"always_redirect": false,
|
||||
"cert_file": "",
|
||||
"chain_file": "",
|
||||
"key_file": ""
|
||||
},
|
||||
"cors": {
|
||||
"enabled": false,
|
||||
"corsorigin": "*"
|
||||
}
|
||||
},
|
||||
|
||||
"coin": {
|
||||
"name": "${COIN_NAME}",
|
||||
"symbol": "${COIN_SYMBOL}"
|
||||
},
|
||||
|
||||
"network_history": {
|
||||
"enabled": true,
|
||||
"max_saved_records": 0,
|
||||
"max_hours": 24
|
||||
},
|
||||
|
||||
"shared_pages": {
|
||||
"theme": "Purple",
|
||||
"page_title": "${COIN_NAME} Explorer",
|
||||
"favicons": {
|
||||
"favicon32": "favicon-32.png",
|
||||
"favicon128": "favicon-128.png",
|
||||
"favicon180": "favicon-180.png",
|
||||
"favicon192": "favicon-192.png"
|
||||
},
|
||||
"logo": "/img/logo.png",
|
||||
"date_time": {
|
||||
"display_format": "LLL dd, yyyy HH:mm:ss ZZZZ",
|
||||
"timezone": "utc",
|
||||
"enable_alt_timezone_tooltips": false
|
||||
},
|
||||
"table_header_bgcolor": "",
|
||||
"confirmations": 6,
|
||||
"difficulty": "${COIN_DIFFICULTY}",
|
||||
"show_hashrate": true,
|
||||
"page_header": {
|
||||
"menu": "side",
|
||||
"sticky_header": true,
|
||||
"bgcolor": "",
|
||||
"home_link": "coin",
|
||||
"home_link_logo": "",
|
||||
"home_link_logo_height": 50,
|
||||
"panels": {
|
||||
"network_panel": {
|
||||
"enabled": true,
|
||||
"display_order": 1,
|
||||
"nethash": "getnetworkhashps",
|
||||
"nethash_units": "G"
|
||||
},
|
||||
"difficulty_panel": {
|
||||
"enabled": true,
|
||||
"display_order": 2
|
||||
},
|
||||
"masternodes_panel": {
|
||||
"enabled": false,
|
||||
"display_order": 0
|
||||
},
|
||||
"coin_supply_panel": {
|
||||
"enabled": true,
|
||||
"display_order": 3
|
||||
},
|
||||
"price_panel": {
|
||||
"enabled": false,
|
||||
"display_order": 0,
|
||||
"coingecko_id": ""
|
||||
}
|
||||
},
|
||||
"block_count_panel": {
|
||||
"enabled": true,
|
||||
"display_order": 4
|
||||
},
|
||||
"network_charts": {
|
||||
"nethash_chart": {
|
||||
"enabled": true,
|
||||
"chart_title": { "enabled": false, "title_text": "", "alignment": "center", "color": "#666", "font": { "family": "Arial", "size": 14, "weight": "bold" } },
|
||||
"legend": { "enabled": true, "position": "bottom" },
|
||||
"bgcolor": "transparent",
|
||||
"line_color": "rgba(54,162,235,1)",
|
||||
"fill_color": "rgba(54,162,235,0.2)",
|
||||
"crosshair_color": "#aaa",
|
||||
"block_line": { "enabled": true, "block_line_color": "rgba(0,128,0,0.2)" },
|
||||
"round_decimals": 3,
|
||||
"chart_height": 320,
|
||||
"full_row": false,
|
||||
"stretch_to_fit": false
|
||||
},
|
||||
"difficulty_chart": {
|
||||
"enabled": true,
|
||||
"chart_title": { "enabled": false, "title_text": "", "alignment": "center", "color": "#666", "font": { "family": "Arial", "size": 14, "weight": "bold" } },
|
||||
"legend": { "enabled": true, "position": "bottom" },
|
||||
"bgcolor": "transparent",
|
||||
"pow_line_color": "rgba(255,99,132,1)",
|
||||
"pow_fill_color": "rgba(255,99,132,0.2)",
|
||||
"pos_line_color": "rgba(255,161,0,1)",
|
||||
"pos_fill_color": "rgba(255,161,0,0.2)",
|
||||
"crosshair_color": "#aaa",
|
||||
"block_line": { "enabled": true, "block_line_color": "rgba(0,128,0,0.2)" },
|
||||
"round_decimals": 3,
|
||||
"chart_height": 320,
|
||||
"full_row": false,
|
||||
"stretch_to_fit": false
|
||||
},
|
||||
"reload_chart_seconds": 60,
|
||||
"sync_charts": true
|
||||
},
|
||||
"custom_menus": []
|
||||
},
|
||||
"page_footer": {
|
||||
"enabled": true,
|
||||
"bgcolor": "",
|
||||
"shortcut_menus": []
|
||||
},
|
||||
"search": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
|
||||
"index_page": {
|
||||
"show_panels": true,
|
||||
"show_nethash_chart": true,
|
||||
"show_difficulty_chart": true,
|
||||
"page_header": {
|
||||
"show_panels": true,
|
||||
"show_nethash_chart": true,
|
||||
"show_difficulty_chart": true
|
||||
},
|
||||
"transaction_count": 20
|
||||
},
|
||||
|
||||
"block_page": {
|
||||
"show_panels": false,
|
||||
"genesis_block": "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
|
||||
"transaction_page": {
|
||||
"show_panels": false,
|
||||
"show_unconfirmed_warning_banner": true
|
||||
},
|
||||
|
||||
"address_page": {
|
||||
"show_panels": false,
|
||||
"enable_hidden_address_view": false,
|
||||
"enable_claim_address_feature": false
|
||||
},
|
||||
|
||||
"richlist_page": {
|
||||
"show_panels": false,
|
||||
"enabled": true,
|
||||
"show_received_column": true,
|
||||
"show_balance_column": true,
|
||||
"show_percent_column": true,
|
||||
"show_sent_column": false
|
||||
},
|
||||
|
||||
"movement_page": {
|
||||
"show_panels": false,
|
||||
"enabled": true,
|
||||
"min_amount": 100,
|
||||
"low_supply": false
|
||||
},
|
||||
|
||||
"network_page": {
|
||||
"show_panels": false,
|
||||
"enabled": true,
|
||||
"nethash_chart": {
|
||||
"enabled": true,
|
||||
"chart_days": 30
|
||||
},
|
||||
"difficulty_chart": {
|
||||
"enabled": true,
|
||||
"chart_days": 30
|
||||
}
|
||||
},
|
||||
|
||||
"masternodes_page": {
|
||||
"show_panels": false,
|
||||
"enabled": false
|
||||
},
|
||||
|
||||
"markets_page": {
|
||||
"show_panels": false,
|
||||
"enabled": false,
|
||||
"default_exchange": "",
|
||||
"default_exchange_currency": "",
|
||||
"exchanges": {}
|
||||
},
|
||||
|
||||
"api_page": {
|
||||
"show_panels": false,
|
||||
"enabled": true,
|
||||
"public_apis": {
|
||||
"rpc": {
|
||||
"getdifficulty": { "enabled": true },
|
||||
"getconnectioncount": { "enabled": true },
|
||||
"getblockcount": { "enabled": true },
|
||||
"getblockhash": { "enabled": true },
|
||||
"getblock": { "enabled": true },
|
||||
"getrawtransaction": { "enabled": true },
|
||||
"getnetworkhashps": { "enabled": true }
|
||||
},
|
||||
"ext": {
|
||||
"getmoneysupply": { "enabled": true },
|
||||
"getdistribution": { "enabled": true },
|
||||
"getaddress": { "enabled": true },
|
||||
"getaddresstxs": { "enabled": true },
|
||||
"gettx": { "enabled": true },
|
||||
"getbalance": { "enabled": true },
|
||||
"getlasttxs": { "enabled": true },
|
||||
"getcurrentprice": { "enabled": true },
|
||||
"getnetworkpeers": { "enabled": true },
|
||||
"getbasicstats": { "enabled": true },
|
||||
"getsummary": { "enabled": true },
|
||||
"getmasternodelist": { "enabled": false },
|
||||
"getmasternodecount": { "enabled": false }
|
||||
}
|
||||
},
|
||||
"api_rate_limit": {
|
||||
"enabled": true,
|
||||
"limit_safe_count": 15,
|
||||
"limit_expensive_count": 5
|
||||
}
|
||||
},
|
||||
|
||||
"orphans_page": {
|
||||
"show_panels": false,
|
||||
"enabled": true
|
||||
},
|
||||
|
||||
"sync": {
|
||||
"block_parallel_tasks": 1,
|
||||
"supply": "TXOUTSET"
|
||||
}
|
||||
}
|
||||
@@ -1 +1,216 @@
|
||||
/* Add custom css rules here */
|
||||
/* ============================================================
|
||||
BitcoinPurple Explorer – explorer-specific component styles
|
||||
The base dark theme is in themes/purple/_bootswatch.scss.
|
||||
This file adds only what Bootstrap + bootswatch don't cover.
|
||||
============================================================ */
|
||||
|
||||
/* ── CSS vars (available to JS and inline styles) ────────────── */
|
||||
:root {
|
||||
--btcp-bg: #06000f;
|
||||
--btcp-surface: #110028;
|
||||
--btcp-card: #1d0845;
|
||||
--btcp-header: #3a158a;
|
||||
--btcp-border: #7c3aed;
|
||||
--btcp-accent: #9333ea;
|
||||
--btcp-bright: #a855f7;
|
||||
--btcp-label: #c084fc;
|
||||
--btcp-text: #ece0ff;
|
||||
--btcp-text-dim: #9d6ae8;
|
||||
--btcp-glow: 0 0 14px rgba(147,51,234,.5), 0 0 40px rgba(147,51,234,.18);
|
||||
--btcp-glow-sm: 0 0 8px rgba(147,51,234,.4);
|
||||
--radius: .55rem;
|
||||
--radius-pill: 50rem;
|
||||
}
|
||||
|
||||
/* ── Search bar ──────────────────────────────────────────────── */
|
||||
.search-box-custom, #index-search, #search-row { padding: .75rem 0; }
|
||||
|
||||
.search-for,
|
||||
#index-search .form-group {
|
||||
width: 100%;
|
||||
max-width: 680px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Input field */
|
||||
.search-for .form-control,
|
||||
#index-search input.form-control {
|
||||
background: var(--btcp-surface) !important;
|
||||
border: 1.5px solid var(--btcp-border) !important;
|
||||
border-right: none !important;
|
||||
border-radius: var(--radius-pill) 0 0 var(--radius-pill) !important;
|
||||
color: #fff !important;
|
||||
padding: .6rem 1.25rem .6rem 2.6rem !important;
|
||||
font-size: .95rem;
|
||||
font-weight: 500;
|
||||
height: 44px;
|
||||
caret-color: var(--btcp-label);
|
||||
transition: border-color .2s, box-shadow .2s;
|
||||
}
|
||||
.search-for .form-control:focus,
|
||||
#index-search input.form-control:focus {
|
||||
border-color: var(--btcp-accent) !important;
|
||||
box-shadow: 0 0 0 3px rgba(147,51,234,.28) !important;
|
||||
background: #16003a !important;
|
||||
color: #fff !important;
|
||||
outline: none;
|
||||
}
|
||||
.search-for .form-control::placeholder,
|
||||
#index-search input.form-control::placeholder {
|
||||
color: rgba(192,132,252,.7);
|
||||
}
|
||||
|
||||
/* Search icon */
|
||||
.search-for::before {
|
||||
content: "⌕";
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--btcp-label);
|
||||
font-size: 1.1rem;
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
/* btn-success → purple (used as search button) */
|
||||
.btn-success,
|
||||
.btn-success:not(:disabled):not(.disabled) {
|
||||
background: linear-gradient(135deg, var(--btcp-border), var(--btcp-accent)) !important;
|
||||
border: none !important;
|
||||
color: #fff !important;
|
||||
font-weight: 600;
|
||||
box-shadow: var(--btcp-glow-sm);
|
||||
transition: all .2s ease;
|
||||
}
|
||||
.btn-success:hover,
|
||||
.btn-success:focus {
|
||||
background: linear-gradient(135deg, var(--btcp-accent), var(--btcp-bright)) !important;
|
||||
box-shadow: var(--btcp-glow) !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.btn-success:active { transform: translateY(0); }
|
||||
|
||||
.search-for .btn-success,
|
||||
#index-search .btn-success {
|
||||
border-radius: 0 var(--radius-pill) var(--radius-pill) 0 !important;
|
||||
padding: .6rem 1.5rem !important;
|
||||
height: 44px;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
/* ── Stat panels (header area) ───────────────────────────────── */
|
||||
.panel-box,
|
||||
.card.panel,
|
||||
.stat-panel {
|
||||
border: 1px solid rgba(124,58,237,.55) !important;
|
||||
background: var(--btcp-card) !important;
|
||||
border-radius: var(--radius) !important;
|
||||
transition: transform .18s, box-shadow .18s;
|
||||
}
|
||||
.panel-box:hover,
|
||||
.card.panel:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--btcp-glow) !important;
|
||||
}
|
||||
|
||||
/* Big numbers */
|
||||
.panel-box .panel-value,
|
||||
.card .display-4,
|
||||
.card h3, .card .h3 {
|
||||
color: #fff !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Labels */
|
||||
.panel-box .panel-label,
|
||||
.card .card-title,
|
||||
.card small {
|
||||
color: var(--btcp-label) !important;
|
||||
font-size: .73rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .09em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Text helpers ────────────────────────────────────────────── */
|
||||
body { color: var(--btcp-text) !important; }
|
||||
.card-body, .panel-body { color: var(--btcp-text) !important; }
|
||||
p, li, span, div, td, th, label { color: inherit; }
|
||||
|
||||
.text-muted, small, .small {
|
||||
color: var(--btcp-text-dim) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* ── Hash / monospace links in tables ────────────────────────── */
|
||||
.table td a {
|
||||
font-family: "SF Mono", "Fira Code", monospace;
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
/* ── Status badges ───────────────────────────────────────────── */
|
||||
.text-success { color: #4ade80 !important; }
|
||||
.text-danger { color: #f87171 !important; }
|
||||
.text-warning { color: #fbbf24 !important; }
|
||||
|
||||
/* ── Loading bar ─────────────────────────────────────────────── */
|
||||
#loading-bar, .loading-bar, #nprogress .bar {
|
||||
background: linear-gradient(90deg, var(--btcp-border), var(--btcp-label)) !important;
|
||||
box-shadow: 0 0 8px var(--btcp-accent);
|
||||
}
|
||||
|
||||
/* ── Chart canvas subtle glow ────────────────────────────────── */
|
||||
canvas {
|
||||
filter: drop-shadow(0 0 4px rgba(147,51,234,.12));
|
||||
}
|
||||
|
||||
/* ── Network chart card overrides ────────────────────────────── */
|
||||
#nethashChartParent,
|
||||
#difficultyChartParent {
|
||||
.card { background: var(--btcp-card) !important; overflow: hidden; }
|
||||
.card-header {
|
||||
background: var(--btcp-header) !important;
|
||||
border-bottom: 2px solid var(--btcp-border) !important;
|
||||
padding: .6rem 1rem !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(180deg, var(--btcp-label), var(--btcp-accent));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #fff !important;
|
||||
font-size: .78rem;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
.card-body { padding: .75rem !important; }
|
||||
canvas { display: block; border-radius: .3rem; }
|
||||
}
|
||||
|
||||
/* ── Scrollbar ───────────────────────────────────────────────── */
|
||||
* { scrollbar-width: thin; scrollbar-color: var(--btcp-border) var(--btcp-bg); }
|
||||
::-webkit-scrollbar { width: 5px; height: 5px; }
|
||||
::-webkit-scrollbar-track { background: var(--btcp-bg); }
|
||||
::-webkit-scrollbar-thumb { background: var(--btcp-border); border-radius: 99px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--btcp-accent); }
|
||||
|
||||
/* ── Tooltip ─────────────────────────────────────────────────── */
|
||||
.tooltip-inner {
|
||||
background: var(--btcp-card) !important;
|
||||
border: 1px solid var(--btcp-border);
|
||||
color: var(--btcp-text);
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
@use './themes/united/variables' as united;
|
||||
@use './themes/vapor/variables' as vapor;
|
||||
@use './themes/yeti/variables' as yeti;
|
||||
@use './themes/purple/variables' as purple;
|
||||
@use './themes/zephyr/variables' as zephyr;
|
||||
|
||||
$theme-name: string.to-lower-case(theme-selector.$theme-name);
|
||||
@@ -79,6 +80,9 @@ $theme-name: string.to-lower-case(theme-selector.$theme-name);
|
||||
border-color: united.$gray-300 if($important == 0, null, !important);
|
||||
} @else if $theme-name == "vapor" {
|
||||
border-color: vapor.$gray-300 if($important == 0, null, !important);
|
||||
|
||||
} @else if $theme-name == "purple" {
|
||||
border-color: btcp.$gray-300 if($important == 0, null, !important);
|
||||
} @else if $theme-name == "yeti" {
|
||||
border-color: rgba(0, 0, 0, 0.1) if($important == 0, null, !important); /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "zephyr" {
|
||||
@@ -137,6 +141,9 @@ $theme-name: string.to-lower-case(theme-selector.$theme-name);
|
||||
border-color: #853e64; /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "vapor" {
|
||||
border-color: #2e1b3e; /* Hardcoded value not present in _variables.scss */
|
||||
|
||||
} @else if $theme-name == "purple" {
|
||||
border-color: #3d1278; /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "yeti" {
|
||||
border-color: #383838; /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "zephyr" {
|
||||
@@ -195,6 +202,9 @@ $theme-name: string.to-lower-case(theme-selector.$theme-name);
|
||||
border-color: #d2d4d7; /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "vapor" {
|
||||
border-color: #57ddea; /* Hardcoded value not present in _variables.scss */
|
||||
|
||||
} @else if $theme-name == "purple" {
|
||||
border-color: #9333ea; /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "yeti" {
|
||||
border-color: #d6d6d6; /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "zephyr" {
|
||||
@@ -277,6 +287,10 @@ $theme-name: string.to-lower-case(theme-selector.$theme-name);
|
||||
} @else if $theme-name == "vapor" {
|
||||
color: vapor.$white;
|
||||
background-color: vapor.$gray-900;
|
||||
|
||||
} @else if $theme-name == "purple" {
|
||||
color: btcp.$white;
|
||||
background-color: btcp.$gray-900;
|
||||
} @else if $theme-name == "yeti" {
|
||||
color: yeti.$white;
|
||||
background-color: yeti.$gray-900;
|
||||
@@ -364,6 +378,10 @@ $theme-name: string.to-lower-case(theme-selector.$theme-name);
|
||||
} @else if $theme-name == "vapor" {
|
||||
color: vapor.$white;
|
||||
background-color: vapor.$light;
|
||||
|
||||
} @else if $theme-name == "purple" {
|
||||
color: btcp.$white;
|
||||
background-color: btcp.$light;
|
||||
} @else if $theme-name == "yeti" {
|
||||
color: yeti.$black;
|
||||
background-color: yeti.$gray-200;
|
||||
@@ -427,6 +445,9 @@ $theme-name: string.to-lower-case(theme-selector.$theme-name);
|
||||
color: rgba(255, 255, 255, 0.55); /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "vapor" {
|
||||
color: rgba(255, 255, 255, 0.55); /* Hardcoded value not present in _variables.scss */
|
||||
|
||||
} @else if $theme-name == "purple" {
|
||||
color: rgba(255, 255, 255, 0.55); /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "yeti" {
|
||||
color: rgba(255, 255, 255, 0.7); /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "zephyr" {
|
||||
@@ -485,6 +506,9 @@ $theme-name: string.to-lower-case(theme-selector.$theme-name);
|
||||
color: rgba(0, 0, 0, 0.55); /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "vapor" {
|
||||
color: rgba(0, 0, 0, 0.55); /* Hardcoded value not present in _variables.scss */
|
||||
|
||||
} @else if $theme-name == "purple" {
|
||||
color: rgba(0, 0, 0, 0.55); /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "yeti" {
|
||||
color: rgba(0, 0, 0, 0.55); /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "zephyr" {
|
||||
@@ -543,6 +567,9 @@ $theme-name: string.to-lower-case(theme-selector.$theme-name);
|
||||
color: rgba(255, 255, 255, 0.55); /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "vapor" {
|
||||
color: rgba(255, 255, 255, 0.55); /* Hardcoded value not present in _variables.scss */
|
||||
|
||||
} @else if $theme-name == "purple" {
|
||||
color: rgba(255, 255, 255, 0.55); /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "yeti" {
|
||||
color: rgba(255, 255, 255, 0.7); /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "zephyr" {
|
||||
@@ -601,6 +628,9 @@ $theme-name: string.to-lower-case(theme-selector.$theme-name);
|
||||
color: rgba(0, 0, 0, 0.55); /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "vapor" {
|
||||
color: rgba(0, 0, 0, 0.55); /* Hardcoded value not present in _variables.scss */
|
||||
|
||||
} @else if $theme-name == "purple" {
|
||||
color: rgba(0, 0, 0, 0.55); /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "yeti" {
|
||||
color: rgba(0, 0, 0, 0.55); /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "zephyr" {
|
||||
@@ -659,6 +689,9 @@ $theme-name: string.to-lower-case(theme-selector.$theme-name);
|
||||
color: rgba(255, 255, 255, 0.75); /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "vapor" {
|
||||
color: rgba(255, 255, 255, 0.75); /* Hardcoded value not present in _variables.scss */
|
||||
|
||||
} @else if $theme-name == "purple" {
|
||||
color: rgba(255, 255, 255, 0.75); /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "yeti" {
|
||||
color: yeti.$white;
|
||||
} @else if $theme-name == "zephyr" {
|
||||
@@ -718,6 +751,9 @@ $theme-name: string.to-lower-case(theme-selector.$theme-name);
|
||||
color: rgba(0, 0, 0, 0.7); /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "vapor" {
|
||||
color: rgba(0, 0, 0, 0.7); /* Hardcoded value not present in _variables.scss */
|
||||
|
||||
} @else if $theme-name == "purple" {
|
||||
color: rgba(0, 0, 0, 0.7); /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "yeti" {
|
||||
color: rgba(0, 0, 0, 0.7); /* Hardcoded value not present in _variables.scss */
|
||||
} @else if $theme-name == "zephyr" {
|
||||
@@ -777,6 +813,9 @@ $theme-name: string.to-lower-case(theme-selector.$theme-name);
|
||||
background: united.$primary;
|
||||
} @else if $theme-name == "vapor" {
|
||||
background: vapor.$primary;
|
||||
|
||||
} @else if $theme-name == "purple" {
|
||||
background: btcp.$primary;
|
||||
} @else if $theme-name == "yeti" {
|
||||
background: yeti.$primary;
|
||||
} @else if $theme-name == "zephyr" {
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
// BitcoinPurple – Bootswatch overrides
|
||||
// Handles everything Bootstrap's SCSS variables can't reach
|
||||
|
||||
@use "sass:color";
|
||||
|
||||
// ── Web font ──────────────────────────────────────────────────────────────────
|
||||
$web-font-path: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" !default;
|
||||
@if $web-font-path {
|
||||
@import url($web-font-path);
|
||||
}
|
||||
|
||||
// ── Mixins ────────────────────────────────────────────────────────────────────
|
||||
@mixin purple-glow($size: 8px) {
|
||||
box-shadow: 0 0 $size rgba(168, 85, 247, .55), 0 0 #{$size * 3} rgba(168, 85, 247, .2);
|
||||
}
|
||||
|
||||
// ── Body ──────────────────────────────────────────────────────────────────────
|
||||
body {
|
||||
background: #06000f !important;
|
||||
}
|
||||
|
||||
// ── Headings ─────────────────────────────────────────────────────────────────
|
||||
h1, h2, h3, h4, h5, h6,
|
||||
.h1, .h2, .h3, .h4, .h5, .h6 {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
// ── Navbar ────────────────────────────────────────────────────────────────────
|
||||
.navbar {
|
||||
background: #110028 !important;
|
||||
border-bottom: 2px solid #7c3aed;
|
||||
box-shadow: 0 2px 20px rgba(0, 0, 0, .8);
|
||||
}
|
||||
|
||||
.navbar-brand { color: #fff !important; font-weight: 700; }
|
||||
|
||||
.nav-link {
|
||||
color: rgba(236, 224, 255, .65) !important;
|
||||
border-radius: .35rem;
|
||||
transition: color .15s, background .15s;
|
||||
|
||||
&:hover, &.active {
|
||||
color: #fff !important;
|
||||
background: rgba(168, 85, 247, .2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sidebar ───────────────────────────────────────────────────────────────────
|
||||
#sidebar,
|
||||
.sidebar,
|
||||
[id*="sidebar"],
|
||||
.nav-sidebar,
|
||||
.col-sidebar {
|
||||
background: #110028 !important;
|
||||
border-right: 2px solid #7c3aed;
|
||||
}
|
||||
|
||||
// ── Cards ─────────────────────────────────────────────────────────────────────
|
||||
.card {
|
||||
background: #1d0845 !important;
|
||||
border: 1px solid rgba(124, 58, 237, .55) !important;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: #3a158a !important;
|
||||
border-bottom: 2px solid rgba(124, 58, 237, .7) !important;
|
||||
color: #fff !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// ── Tables ────────────────────────────────────────────────────────────────────
|
||||
.table {
|
||||
color: #ece0ff;
|
||||
|
||||
thead th {
|
||||
background: #3a158a !important;
|
||||
color: #fff !important;
|
||||
border-bottom: 2px solid #7c3aed !important;
|
||||
border-top: none !important;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
font-size: .78rem;
|
||||
letter-spacing: .07em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td { border-color: rgba(124, 58, 237, .3) !important; }
|
||||
|
||||
tbody tr:nth-child(odd) td { background: rgba(58, 21, 138, .2) !important; }
|
||||
tbody tr:nth-child(even) td { background: rgba(17, 0, 40, .15) !important; }
|
||||
tbody tr:hover td { background: rgba(168, 85, 247, .15) !important; }
|
||||
|
||||
// Monospace hash links
|
||||
td a {
|
||||
color: #c084fc;
|
||||
&:hover { color: #fff; text-shadow: 0 0 8px rgba(192, 132, 252, .8); }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Buttons ───────────────────────────────────────────────────────────────────
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #7c3aed, #9333ea) !important;
|
||||
border: none !important;
|
||||
|
||||
&:hover, &:focus {
|
||||
background: linear-gradient(135deg, #9333ea, #a855f7) !important;
|
||||
@include purple-glow(10px);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #3a158a !important;
|
||||
border: 1px solid #7c3aed !important;
|
||||
color: #ece0ff !important;
|
||||
}
|
||||
|
||||
// ── Badges ────────────────────────────────────────────────────────────────────
|
||||
.badge.bg-primary { background: #7c3aed !important; }
|
||||
.badge.bg-secondary { background: #3a158a !important; }
|
||||
|
||||
.badge.bg-success {
|
||||
background: rgba(74, 222, 128, .18) !important;
|
||||
color: #4ade80 !important;
|
||||
border: 1px solid rgba(74, 222, 128, .4) !important;
|
||||
}
|
||||
|
||||
.badge.bg-danger {
|
||||
background: rgba(248, 113, 113, .18) !important;
|
||||
color: #f87171 !important;
|
||||
border: 1px solid rgba(248, 113, 113, .4) !important;
|
||||
}
|
||||
|
||||
// ── Inputs ───────────────────────────────────────────────────────────────────
|
||||
.form-control,
|
||||
.form-select {
|
||||
background: #110028 !important;
|
||||
border-color: rgba(124, 58, 237, .55) !important;
|
||||
color: #ece0ff !important;
|
||||
|
||||
&::placeholder { color: rgba(192, 132, 252, .6); }
|
||||
|
||||
&:focus {
|
||||
background: #110028 !important;
|
||||
border-color: #9333ea !important;
|
||||
box-shadow: 0 0 0 3px rgba(147, 51, 234, .25) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pagination ────────────────────────────────────────────────────────────────
|
||||
.page-link {
|
||||
background: #1d0845 !important;
|
||||
border-color: rgba(124, 58, 237, .5) !important;
|
||||
color: #c084fc !important;
|
||||
transition: all .15s;
|
||||
|
||||
&:hover {
|
||||
background: #7c3aed !important;
|
||||
border-color: #7c3aed !important;
|
||||
color: #fff !important;
|
||||
@include purple-glow(6px);
|
||||
}
|
||||
}
|
||||
|
||||
.page-item.active .page-link {
|
||||
background: linear-gradient(135deg, #7c3aed, #9333ea) !important;
|
||||
border-color: transparent !important;
|
||||
color: #fff !important;
|
||||
@include purple-glow(6px);
|
||||
}
|
||||
|
||||
// ── Scrollbar ─────────────────────────────────────────────────────────────────
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: #06000f; }
|
||||
::-webkit-scrollbar-thumb { background: #7c3aed; border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #9333ea; }
|
||||
|
||||
// ── Breadcrumbs ───────────────────────────────────────────────────────────────
|
||||
.breadcrumb { background: #1d0845 !important; }
|
||||
.breadcrumb-item a { color: #c084fc !important; }
|
||||
|
||||
// ── Code / pre ────────────────────────────────────────────────────────────────
|
||||
code {
|
||||
color: #f0abfc;
|
||||
background: rgba(58, 21, 138, .5);
|
||||
padding: .1em .3em;
|
||||
border-radius: .2em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #110028;
|
||||
border: 1px solid rgba(124, 58, 237, .4);
|
||||
color: #ece0ff;
|
||||
}
|
||||
|
||||
// ── Footer ────────────────────────────────────────────────────────────────────
|
||||
footer,
|
||||
.footer {
|
||||
background: #110028 !important;
|
||||
border-top: 2px solid #7c3aed !important;
|
||||
color: rgba(168, 85, 247, .8) !important;
|
||||
}
|
||||
|
||||
// ── Alerts ───────────────────────────────────────────────────────────────────
|
||||
.alert {
|
||||
border: none;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
// ── Misc ──────────────────────────────────────────────────────────────────────
|
||||
.text-muted { color: #9d6ae8 !important; }
|
||||
|
||||
hr {
|
||||
border-color: rgba(124, 58, 237, .4);
|
||||
opacity: .6;
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
// BitcoinPurple Theme
|
||||
// Contrast-aware dark palette inspired by Darkly + Cyborg structure
|
||||
//
|
||||
// Luminance scale (each level ~3-5x the previous):
|
||||
// $gray-900 #06000f 0.1% body background
|
||||
// $gray-800 #110028 0.6% navbar / sidebar
|
||||
// $gray-700 #1d0845 2.4% card body
|
||||
// $gray-600 #3a158a 7.3% card header / table thead
|
||||
// $gray-500 #7c3aed 18% borders / interactive
|
||||
// $gray-400 #9d6ae8 31% secondary interactive
|
||||
// $gray-300 #c084fc 47% links / accent labels
|
||||
// $gray-200 #ddb8ff 63% secondary text
|
||||
// $gray-100 #f0e8ff 79% near-white text
|
||||
//
|
||||
// Key contrast ratios:
|
||||
// body text (#ece0ff) on bg (#06000f) → 16:1 ✓ AAA
|
||||
// white on card header (#3a158a) → 8.5:1 ✓ AAA
|
||||
// white on primary (#9333ea) → 5.5:1 ✓ AA
|
||||
// link (#c084fc) on bg (#06000f) → 10:1 ✓ AAA
|
||||
// link (#c084fc) on card (#1d0845) → 7:1 ✓ AA
|
||||
|
||||
@use "sass:color";
|
||||
|
||||
$theme: "purple" !default;
|
||||
|
||||
// ── Color scale ───────────────────────────────────────────────────────────────
|
||||
$white: #fff !default;
|
||||
$gray-100: #f0e8ff !default;
|
||||
$gray-200: #ddb8ff !default;
|
||||
$gray-300: #c084fc !default;
|
||||
$gray-400: #9d6ae8 !default;
|
||||
$gray-500: #7c3aed !default;
|
||||
$gray-600: #3a158a !default;
|
||||
$gray-700: #1d0845 !default;
|
||||
$gray-800: #110028 !default;
|
||||
$gray-900: #06000f !default;
|
||||
$black: #020008 !default;
|
||||
|
||||
// ── Named colors ──────────────────────────────────────────────────────────────
|
||||
$blue: #60a5fa !default;
|
||||
$indigo: #818cf8 !default;
|
||||
$purple: #c084fc !default;
|
||||
$pink: #f0abfc !default;
|
||||
$red: #f87171 !default;
|
||||
$orange: #fb923c !default;
|
||||
$yellow: #fbbf24 !default;
|
||||
$green: #4ade80 !default;
|
||||
$teal: #2dd4bf !default;
|
||||
$cyan: #22d3ee !default;
|
||||
|
||||
// ── Theme colors ──────────────────────────────────────────────────────────────
|
||||
// Primary: #9333ea → white contrast 5.5:1 (AA ✓)
|
||||
$primary: #9333ea !default;
|
||||
$secondary: $gray-600 !default;
|
||||
$success: $green !default;
|
||||
$info: $blue !default;
|
||||
$warning: $yellow !default;
|
||||
$danger: $red !default;
|
||||
$light: $gray-200 !default;
|
||||
$dark: $gray-900 !default;
|
||||
|
||||
$min-contrast-ratio: 2.5 !default;
|
||||
|
||||
// ── Body ──────────────────────────────────────────────────────────────────────
|
||||
$body-bg: $gray-900 !default;
|
||||
$body-color: #ece0ff !default; // 16:1 contrast on body-bg
|
||||
|
||||
// ── Links ─────────────────────────────────────────────────────────────────────
|
||||
$link-color: $gray-300 !default;
|
||||
$link-hover-color: $white !default;
|
||||
$link-decoration: none !default;
|
||||
$link-hover-decoration: underline !default;
|
||||
|
||||
// ── Typography ────────────────────────────────────────────────────────────────
|
||||
$font-family-sans-serif: "Inter", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !default;
|
||||
$text-muted: $gray-400 !default;
|
||||
|
||||
// ── Tables ────────────────────────────────────────────────────────────────────
|
||||
$table-color: $body-color !default;
|
||||
$table-border-color: rgba($gray-500, .35) !default;
|
||||
$table-bg-scale: 0 !default;
|
||||
|
||||
// ── Forms ─────────────────────────────────────────────────────────────────────
|
||||
$input-bg: $gray-800 !default;
|
||||
$input-color: $gray-100 !default;
|
||||
$input-border-color: rgba($gray-500, .6) !default;
|
||||
$input-placeholder-color: rgba($gray-300, .6) !default;
|
||||
$input-group-addon-color: $gray-200 !default;
|
||||
$input-group-addon-bg: $gray-600 !default;
|
||||
|
||||
$form-check-input-bg: $gray-800 !default;
|
||||
$form-check-input-border: 1px solid rgba($gray-500, .6) !default;
|
||||
|
||||
// ── Nav ───────────────────────────────────────────────────────────────────────
|
||||
$nav-link-padding-x: 1.2rem !default;
|
||||
$nav-link-disabled-color: $gray-500 !default;
|
||||
$nav-tabs-border-color: $gray-500 !default;
|
||||
$nav-tabs-link-active-color:$white !default;
|
||||
|
||||
// ── Navbar ────────────────────────────────────────────────────────────────────
|
||||
$navbar-dark-color: rgba($white, .7) !default;
|
||||
$navbar-dark-hover-color: $white !default;
|
||||
|
||||
// ── Dropdowns ────────────────────────────────────────────────────────────────
|
||||
$dropdown-bg: $gray-700 !default;
|
||||
$dropdown-border-color: $gray-500 !default;
|
||||
$dropdown-divider-bg: $gray-600 !default;
|
||||
$dropdown-link-color: $gray-200 !default;
|
||||
$dropdown-link-hover-color: $white !default;
|
||||
$dropdown-link-hover-bg: $primary !default;
|
||||
|
||||
// ── Pagination ────────────────────────────────────────────────────────────────
|
||||
$pagination-color: $white !default;
|
||||
$pagination-bg: $gray-700 !default;
|
||||
$pagination-border-width: 1px !default;
|
||||
$pagination-border-color: rgba($gray-500, .55) !default;
|
||||
$pagination-hover-color: $white !default;
|
||||
$pagination-hover-bg: $primary !default;
|
||||
$pagination-hover-border-color: $primary !default;
|
||||
$pagination-active-bg: $primary !default;
|
||||
$pagination-active-border-color: $primary !default;
|
||||
$pagination-disabled-color: $gray-500 !default;
|
||||
$pagination-disabled-bg: rgba($gray-700, .5) !default;
|
||||
$pagination-disabled-border-color:rgba($gray-500, .3) !default;
|
||||
|
||||
// ── Cards ─────────────────────────────────────────────────────────────────────
|
||||
$card-cap-bg: $gray-600 !default;
|
||||
$card-bg: $gray-700 !default;
|
||||
$card-border-color: rgba($gray-500, .55) !default;
|
||||
$card-cap-color: $white !default;
|
||||
|
||||
// ── Modals ────────────────────────────────────────────────────────────────────
|
||||
$modal-content-bg: $gray-700 !default;
|
||||
$modal-content-border-color: $gray-500 !default;
|
||||
$modal-header-border-color: $gray-500 !default;
|
||||
|
||||
// ── Popovers ─────────────────────────────────────────────────────────────────
|
||||
$popover-bg: $gray-700 !default;
|
||||
$popover-header-bg: $gray-600 !default;
|
||||
|
||||
// ── Progress ─────────────────────────────────────────────────────────────────
|
||||
$progress-bg: $gray-600 !default;
|
||||
|
||||
// ── List group ───────────────────────────────────────────────────────────────
|
||||
$list-group-color: $body-color !default;
|
||||
$list-group-bg: $gray-700 !default;
|
||||
$list-group-border-color: $gray-500 !default;
|
||||
$list-group-hover-bg: $gray-600 !default;
|
||||
$list-group-action-hover-color: $white !default;
|
||||
$list-group-action-active-bg: $gray-900 !default;
|
||||
|
||||
// ── Breadcrumbs ───────────────────────────────────────────────────────────────
|
||||
$breadcrumb-padding-y: .375rem !default;
|
||||
$breadcrumb-padding-x: .75rem !default;
|
||||
$breadcrumb-bg: $gray-700 !default;
|
||||
$breadcrumb-border-radius:.25rem !default;
|
||||
|
||||
// ── Misc ──────────────────────────────────────────────────────────────────────
|
||||
$btn-close-color: $white !default;
|
||||
$btn-close-opacity: .5 !default;
|
||||
$btn-close-hover-opacity:1 !default;
|
||||
$pre-color: inherit !default;
|
||||
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 9.7 KiB |
@@ -1,3 +1,173 @@
|
||||
$(document).ready(function() {
|
||||
/* Add custom javascript code here */
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// HASHRATE AUTO-SCALING
|
||||
// The server sends hashrate already divided by nethash_units ("G"),
|
||||
// so values arrive in GH/s. We scale to keep the display 1–999.
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
var HASH_UNITS = [
|
||||
{ div: 1e6, label: 'PH/s' },
|
||||
{ div: 1e3, label: 'TH/s' },
|
||||
{ div: 1, label: 'GH/s' },
|
||||
{ div: 1e-3, label: 'MH/s' },
|
||||
{ div: 1e-6, label: 'KH/s' },
|
||||
{ div: 1e-9, label: 'H/s' }
|
||||
];
|
||||
|
||||
// Returns {num, unit} where 1 ≤ |num| < 1000 (best fit)
|
||||
function scaleFromGH(gh) {
|
||||
var abs = Math.abs(gh);
|
||||
for (var i = 0; i < HASH_UNITS.length; i++) {
|
||||
if (abs >= HASH_UNITS[i].div) {
|
||||
return { num: gh / HASH_UNITS[i].div, unit: HASH_UNITS[i].label };
|
||||
}
|
||||
}
|
||||
return { num: gh, unit: 'GH/s' };
|
||||
}
|
||||
|
||||
// ── Panel: reformat #hashrate + update unit label ─────────────
|
||||
function refreshHashratePanel() {
|
||||
var $el = $('#hashrate');
|
||||
if (!$el.length) return;
|
||||
|
||||
// Read raw text ("42,650.8351" or similar), strip formatting
|
||||
var raw = $el.text().replace(/[,\s]/g, '');
|
||||
var ghVal = parseFloat(raw);
|
||||
if (!isFinite(ghVal) || ghVal <= 0) return;
|
||||
|
||||
var s = scaleFromGH(ghVal);
|
||||
|
||||
// Format to max 4 significant digits, always 2 decimals
|
||||
var decimals = (s.num >= 100) ? 1 : (s.num >= 10) ? 2 : 3;
|
||||
var formatted = s.num.toFixed(decimals);
|
||||
var parts = formatted.split('.');
|
||||
|
||||
// Temporarily disconnect observer to avoid loop
|
||||
_hashrateObserver.disconnect();
|
||||
$el.html(parts[0] + '.<span class="decimal">' + parts[1] + '</span>');
|
||||
_hashrateObserver.observe($el[0], { childList: true, subtree: true });
|
||||
|
||||
// Update the unit label "(GH/s)" → "(TH/s)" etc.
|
||||
var $unitSpan = $el.closest('.card').find('.card-header span.small');
|
||||
if ($unitSpan.length) $unitSpan.text('(' + s.unit + ')');
|
||||
}
|
||||
|
||||
var _hashrateObserver = new MutationObserver(function() {
|
||||
refreshHashratePanel();
|
||||
});
|
||||
|
||||
// Start watching once #hashrate appears in the DOM
|
||||
var _panelWatcher = new MutationObserver(function(mutations, obs) {
|
||||
var el = document.getElementById('hashrate');
|
||||
if (el) {
|
||||
obs.disconnect();
|
||||
_hashrateObserver.observe(el, { childList: true, subtree: true });
|
||||
}
|
||||
});
|
||||
_panelWatcher.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// CHART.JS IMPROVEMENTS
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
if (typeof Chart === 'undefined') return;
|
||||
|
||||
var font = { family: 'Inter, system-ui, sans-serif', size: 11 };
|
||||
|
||||
// ── Y-axis tick formatters ────────────────────────────────────
|
||||
|
||||
// Generic large-number formatter (difficulty, etc.)
|
||||
function fmtAxis(value) {
|
||||
var abs = Math.abs(value);
|
||||
if (abs >= 1e9) return (value / 1e9).toFixed(1).replace(/\.0$/, '') + 'B';
|
||||
if (abs >= 1e6) return (value / 1e6).toFixed(1).replace(/\.0$/, '') + 'M';
|
||||
if (abs >= 1e3) return (value / 1e3).toFixed(1).replace(/\.0$/, '') + 'K';
|
||||
return value;
|
||||
}
|
||||
|
||||
// Hashrate formatter for nethashChart Y-axis ticks
|
||||
// Scales GH/s values to the 1-999 range with the right unit
|
||||
function fmtHashAxis(gh) {
|
||||
if (gh === 0) return '0';
|
||||
var s = scaleFromGH(gh);
|
||||
var decimals = (Math.abs(s.num) >= 100) ? 0 : (Math.abs(s.num) >= 10) ? 1 : 2;
|
||||
return s.num.toFixed(decimals) + ' ' + s.unit;
|
||||
}
|
||||
|
||||
// ── Linear scale defaults ─────────────────────────────────────
|
||||
var lin = Chart.defaults.scales.linear;
|
||||
if (lin) {
|
||||
lin.ticks = Object.assign({}, lin.ticks, {
|
||||
// 'this' inside callback is the scale; this.chart.canvas.id identifies the chart
|
||||
callback: function(value) {
|
||||
if (this.chart && this.chart.canvas &&
|
||||
this.chart.canvas.id === 'nethashChart') {
|
||||
return fmtHashAxis(value);
|
||||
}
|
||||
return fmtAxis(value);
|
||||
},
|
||||
font: font,
|
||||
color: 'rgba(200,160,255,0.85)',
|
||||
padding: 8,
|
||||
maxTicksLimit: 6
|
||||
});
|
||||
lin.title = Object.assign({}, lin.title, {
|
||||
font: Object.assign({}, font, { weight: '700', size: 11 }),
|
||||
color: 'rgba(216,180,254,1)'
|
||||
});
|
||||
lin.grid = Object.assign({}, lin.grid, {
|
||||
color: 'rgba(80,40,130,0.3)'
|
||||
});
|
||||
}
|
||||
|
||||
// ── Time scale (X-axis) ───────────────────────────────────────
|
||||
['time', 'timeseries'].forEach(function(t) {
|
||||
var s = Chart.defaults.scales[t];
|
||||
if (!s) return;
|
||||
s.ticks = Object.assign({}, s.ticks, {
|
||||
font: font,
|
||||
color: 'rgba(200,160,255,0.75)',
|
||||
maxTicksLimit: 6,
|
||||
maxRotation: 30,
|
||||
minRotation: 0,
|
||||
padding: 6
|
||||
});
|
||||
s.grid = Object.assign({}, s.grid, { color: 'rgba(80,40,130,0.3)' });
|
||||
});
|
||||
|
||||
// ── Plugin: update nethashChart Y-axis title to match auto unit ──
|
||||
Chart.register({
|
||||
id: 'btcpHashrateUnit',
|
||||
afterUpdate: function(chart) {
|
||||
if (!chart.canvas || chart.canvas.id !== 'nethashChart') return;
|
||||
var yScale = chart.scales && chart.scales.y;
|
||||
if (!yScale || !yScale.max) return;
|
||||
|
||||
var s = scaleFromGH(yScale.max);
|
||||
var newTitle = 'Hashrate (' + s.unit + ')';
|
||||
var opts = yScale.options;
|
||||
if (opts && opts.title && opts.title.text !== newTitle) {
|
||||
opts.title.text = newTitle;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Tooltip ───────────────────────────────────────────────────
|
||||
Object.assign(Chart.defaults.plugins.tooltip, {
|
||||
backgroundColor: 'rgba(10,0,28,0.96)',
|
||||
borderColor: 'rgba(93,33,182,0.65)',
|
||||
borderWidth: 1,
|
||||
titleColor: 'rgba(216,180,254,1)',
|
||||
titleFont: Object.assign({}, font, { weight: '700', size: 12 }),
|
||||
bodyColor: 'rgba(240,230,255,0.9)',
|
||||
bodyFont: Object.assign({}, font, { size: 12 }),
|
||||
padding: 11,
|
||||
cornerRadius: 7
|
||||
});
|
||||
|
||||
// ── Legend ────────────────────────────────────────────────────
|
||||
Chart.defaults.plugins.legend.labels.font = Object.assign({}, font, { size: 12 });
|
||||
Chart.defaults.plugins.legend.labels.color = 'rgba(216,180,254,1)';
|
||||
});
|
||||
|
||||