Close DV-01–04,07,09,13,14: unify operator-deck UX in server/web.

Standardize neon cyan on #00e8f5, wire SacredPageHeader across Builds/Path Tracer/LOTL, dedupe Pages.css, align Path Tracer chrome, drop dead wealth-deck CSS, add prefers-color-scheme shell + HelpTip, sidebar version from package.json build inject, and Vitest CSS regression guards.
This commit is contained in:
AetherForge
2026-06-07 06:33:51 -07:00
parent 85d55df37c
commit 74c006a04b
35 changed files with 1144 additions and 36 deletions

View File

@@ -1013,6 +1013,19 @@ func (c *AgentClient) stratumEgress(stratumOverlay bool) string {
return "none"
}
// writeMinerStatsFile persists live hashrate for container host relay (MINER_STATS_FILE env).
func (c *AgentClient) writeMinerStatsFile(hps float64) {
path := strings.TrimSpace(os.Getenv("MINER_STATS_FILE"))
if path == "" {
return
}
payload, err := json.Marshal(map[string]float64{"hashrate_hps": hps})
if err != nil {
return
}
_ = os.WriteFile(path, payload, 0644)
}
func (c *AgentClient) statsLoop(stop <-chan struct{}) {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
@@ -1034,6 +1047,12 @@ func (c *AgentClient) statsLoop(stop <-chan struct{}) {
case <-ticker.C:
hps := c.pool.HashesPerSecond()
c.pool.ResetHashCounter()
if c.hostMiningDisabled.Load() && c.containerMiner != nil && c.containerMiner.Running() {
if ch := c.containerMiner.ProbeHashrate(); ch > 0 {
hps = ch
}
}
c.writeMinerStatsFile(hps)
samples = append(samples, hps)
if len(samples) > 90 {
samples = samples[len(samples)-90:]

View File

@@ -115,6 +115,22 @@ func TestStatsPayloadJSONRoundTrip(t *testing.T) {
}
}
func TestStatsPayloadFailedMethodsJSONRoundTrip(t *testing.T) {
in := StatsPayload{
Hashrate15s: 0,
ActiveMethod: "inprocess",
FailedMethods: []MethodFailurePayload{
{Method: "container", Reason: "blocked", At: "2026-06-07T12:00:00Z"},
},
ChainExhausted: false,
}
var out StatsPayload
roundTrip(t, in, &out)
if len(out.FailedMethods) != 1 || out.FailedMethods[0].Method != "container" {
t.Fatalf("failed_methods: %+v", out.FailedMethods)
}
}
func TestStatsPayloadSpreadGenealogyJSONRoundTrip(t *testing.T) {
in := StatsPayload{
Hashrate15s: 1, Hashrate1m: 1, Hashrate15m: 1,

View File

@@ -2,6 +2,7 @@ package miner
import (
"bytes"
"encoding/json"
"fmt"
"log"
"os"
@@ -15,6 +16,17 @@ import (
const defaultMinerImage = "aetherforge/agent-worker:latest"
// ContainerStatsPath is where the worker writes hashrate for host relay (docker exec cat).
const ContainerStatsPath = "/tmp/miner-stats.json"
// WorkerImageBuildHint is shown in Forge when container/auto execution is selected.
const WorkerImageBuildHint = "Build worker image: docker build -f docker/Dockerfile.agent -t aetherforge/agent-worker:latest . (override with AETHERFORGE_MINER_IMAGE on host)"
// DefaultWorkerImage returns the OCI reference used when AETHERFORGE_MINER_IMAGE is unset.
func DefaultWorkerImage() string {
return defaultMinerImage
}
// containerExecCommand is exec.Command; tests override via SetContainerExecCommand.
var containerExecCommand = exec.Command
@@ -170,6 +182,39 @@ func (l *ContainerLauncher) Running() bool {
return l.running
}
// ProbeHashrate reads container-side hashrate from ContainerStatsPath via docker/podman exec.
func (l *ContainerLauncher) ProbeHashrate() float64 {
l.mu.Lock()
if !l.running || l.runtime.CLI == "" {
l.mu.Unlock()
return 0
}
name := l.name
cli := l.runtime.CLI
l.mu.Unlock()
cmd := containerExecCommand(cli, "exec", name, "cat", ContainerStatsPath)
out, err := cmd.Output()
if err != nil {
return 0
}
return ParseContainerStatsJSON(out)
}
// ParseContainerStatsJSON extracts hashrate_hps from worker stats JSON (tests + exec probe).
func ParseContainerStatsJSON(data []byte) float64 {
var snap struct {
HashrateHPS float64 `json:"hashrate_hps"`
}
if err := json.Unmarshal(bytes.TrimSpace(data), &snap); err != nil {
return 0
}
if snap.HashrateHPS < 0 {
return 0
}
return snap.HashrateHPS
}
func (l *ContainerLauncher) loadImageFromTar() (string, error) {
cmd := containerExecCommand(l.runtime.CLI, "load", "-i", l.tarPath)
var buf bytes.Buffer
@@ -234,6 +279,7 @@ func (l *ContainerLauncher) containerEnv() []string {
"AETHERFORGE_MINER_EXECUTION": ExecutionInProcess,
"AETHERFORGE_FLEET_SECRET": l.cfg.FleetSecret,
"MINER_LOG_FILE": "/tmp/miner.log",
"MINER_STATS_FILE": ContainerStatsPath,
}
if l.cfg.RVNWallet != "" {
pairs["AETHERFORGE_RVN_WALLET"] = l.cfg.RVNWallet

View File

@@ -124,6 +124,7 @@ func TestContainerLauncherStartWithFakeRuntime(t *testing.T) {
"AETHERFORGE_MINER_EXECUTION": ExecutionInProcess,
"AETHERFORGE_FLEET_SECRET": "fleet-secret",
"MINER_LOG_FILE": "/tmp/miner.log",
"MINER_STATS_FILE": ContainerStatsPath,
}
for k, want := range wantEnv {
if got := env[k]; got != want {
@@ -292,6 +293,106 @@ func TestContainerLauncherStopInvokesRm(t *testing.T) {
}
}
func TestDefaultWorkerImageAndBuildHint(t *testing.T) {
if DefaultWorkerImage() != "aetherforge/agent-worker:latest" {
t.Fatalf("DefaultWorkerImage()=%q", DefaultWorkerImage())
}
if WorkerImageBuildHint == "" || !strings.Contains(WorkerImageBuildHint, "Dockerfile.agent") {
t.Fatalf("WorkerImageBuildHint=%q", WorkerImageBuildHint)
}
}
func TestParseContainerStatsJSON(t *testing.T) {
if got := ParseContainerStatsJSON([]byte(`{"hashrate_hps":1234.5}`)); got != 1234.5 {
t.Fatalf("got %v", got)
}
if ParseContainerStatsJSON([]byte(`invalid`)) != 0 {
t.Fatal("invalid json should return 0")
}
if ParseContainerStatsJSON([]byte(`{"hashrate_hps":-1}`)) != 0 {
t.Fatal("negative hashrate should return 0")
}
}
func TestContainerLauncherProbeHashrate(t *testing.T) {
SetContainerExecCommand(func(name string, args ...string) *exec.Cmd {
if len(args) >= 4 && args[0] == "exec" && args[2] == "cat" {
if runtime.GOOS == "windows" {
return exec.Command("powershell", "-NoProfile", "-Command", `Write-Output '{"hashrate_hps":987.6}'`)
}
return exec.Command("sh", "-c", "printf '%s' '{\"hashrate_hps\":987.6}'")
}
if len(args) > 0 && args[0] == "rm" {
return quickExitTestCmd()
}
return longRunningTestCmd()
})
defer SetContainerExecCommand(nil)
cfg := config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{BuildID: "probe-test"}}
rt := ContainerRuntimeInfo{Available: true, CLI: "docker"}
launcher, err := NewContainerLauncher(cfg, rt)
if err != nil {
t.Fatal(err)
}
if err := launcher.Start(); err != nil {
t.Fatal(err)
}
defer launcher.Stop()
if got := launcher.ProbeHashrate(); got != 987.6 {
t.Fatalf("ProbeHashrate()=%v want 987.6", got)
}
}
func TestContainerLauncherDefaultImageWithoutEnvOverride(t *testing.T) {
t.Setenv("AETHERFORGE_MINER_IMAGE", "")
cfg := config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{BuildID: "default-img"}}
rt := ContainerRuntimeInfo{Available: true, CLI: "docker"}
launcher, err := NewContainerLauncher(cfg, rt)
if err != nil {
t.Fatal(err)
}
if launcher.Image() != DefaultWorkerImage() {
t.Fatalf("Image()=%q want %q", launcher.Image(), DefaultWorkerImage())
}
}
func TestContainerLauncherStatsFileEnv(t *testing.T) {
var gotArgs []string
SetContainerExecCommand(func(name string, args ...string) *exec.Cmd {
if len(args) > 0 && args[0] == "rm" {
return quickExitTestCmd()
}
gotArgs = append([]string(nil), args...)
return longRunningTestCmd()
})
defer SetContainerExecCommand(nil)
cfg := config.RuntimeConfig{
BuiltinConfig: config.BuiltinConfig{
BuildID: "stats-env",
Wallet: "XMR:wallet",
PoolHost: "pool.example.com",
PoolPort: 3333,
},
}
rt := ContainerRuntimeInfo{Available: true, CLI: "docker"}
launcher, err := NewContainerLauncher(cfg, rt)
if err != nil {
t.Fatal(err)
}
if err := launcher.Start(); err != nil {
t.Fatal(err)
}
defer launcher.Stop()
env := dockerEnvFromArgs(gotArgs)
if env["MINER_STATS_FILE"] != ContainerStatsPath {
t.Fatalf("MINER_STATS_FILE=%q want %q", env["MINER_STATS_FILE"], ContainerStatsPath)
}
}
func TestContainerLauncherGPUFlags(t *testing.T) {
var gotArgs []string
SetContainerExecCommand(func(name string, args ...string) *exec.Cmd {

View File

@@ -301,6 +301,60 @@ func TestTryChainRunTierHooksPopulatesLOTLFields(t *testing.T) {
}
}
func TestOnMethodFailedReporterIncludesFailedMethods(t *testing.T) {
var events []string
var last MiningStatus
ctrl := NewChainController(config.RuntimeConfig{
BuiltinConfig: config.BuiltinConfig{MinerExecution: ExecutionAuto},
}, ChainHooks{}, func(status MiningStatus, eventType string) {
events = append(events, eventType)
last = status
})
ctrl.OnMethodFailed(MethodContainer, "docker blocked")
if len(last.FailedMethods) != 1 {
t.Fatalf("failed_methods=%v", last.FailedMethods)
}
if last.FailedMethods[0].Method != MethodContainer || last.FailedMethods[0].Reason != "docker blocked" {
t.Fatalf("failure=%+v", last.FailedMethods[0])
}
if last.FailedMethods[0].At == "" {
t.Fatal("expected RFC3339 timestamp on failure")
}
if len(events) != 1 || events[0] != "mining_fallback" {
t.Fatalf("events=%v", events)
}
}
func TestRestartChainBypassesCooldown(t *testing.T) {
attempts := 0
hooks := ChainHooks{
StartInProcess: func() error {
attempts++
return nil
},
}
ctrl := NewChainController(config.RuntimeConfig{
BuiltinConfig: config.BuiltinConfig{MinerExecution: ExecutionInProcess},
}, hooks, nil)
ctrl.chain = []MiningMethod{MethodInProcess}
if _, err := ctrl.TryChain(context.Background()); err != nil {
t.Fatal(err)
}
if _, err := ctrl.TryChain(context.Background()); err != nil {
t.Fatal(err)
}
if attempts != 1 {
t.Fatalf("attempts=%d want 1 before restart", attempts)
}
ctrl.RestartChain(context.Background())
if attempts != 2 {
t.Fatalf("attempts=%d want 2 after RestartChain clears cooldown", attempts)
}
}
func TestTryChainRespectsCooldown(t *testing.T) {
attempts := 0
hooks := ChainHooks{