Make built exe a one-run Windows installer pointing at LAN dashboard.
Install copies miner to AppData, registers autostart, relaunches silently, and builder defaults server URL to local IP.
This commit is contained in:
@@ -31,6 +31,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
||||
h := NewHandler(database)
|
||||
|
||||
r.Get("/health", h.HealthCheck)
|
||||
r.Get("/server/info", h.GetServerInfo)
|
||||
|
||||
// Dashboard
|
||||
r.Get("/dashboard/stats", h.GetDashboardStats)
|
||||
|
||||
115
server/internal/api/server_info.go
Normal file
115
server/internal/api/server_info.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ServerInfo struct {
|
||||
Port int `json:"port"`
|
||||
Host string `json:"host"`
|
||||
LocalIPs []string `json:"local_ips"`
|
||||
SuggestedURL string `json:"suggested_url"`
|
||||
DashboardURL string `json:"dashboard_url"`
|
||||
WebSocketURL string `json:"websocket_url"`
|
||||
}
|
||||
|
||||
func (h *Handler) GetServerInfo(w http.ResponseWriter, r *http.Request) {
|
||||
host := r.Host
|
||||
if idx := strings.Index(host, ":"); idx > 0 {
|
||||
host = host[:idx]
|
||||
}
|
||||
|
||||
localIPs := listLocalIPv4()
|
||||
suggestedHost := host
|
||||
if isLoopbackHost(host) && len(localIPs) > 0 {
|
||||
suggestedHost = localIPs[0]
|
||||
}
|
||||
|
||||
port := 8989
|
||||
if idx := strings.LastIndex(r.Host, ":"); idx > 0 {
|
||||
if p := r.Host[idx+1:]; p != "" {
|
||||
port = parsePort(p)
|
||||
}
|
||||
}
|
||||
|
||||
suggestedURL := "http://" + net.JoinHostPort(suggestedHost, itoa(port))
|
||||
info := ServerInfo{
|
||||
Port: port,
|
||||
Host: host,
|
||||
LocalIPs: localIPs,
|
||||
SuggestedURL: suggestedURL,
|
||||
DashboardURL: suggestedURL,
|
||||
WebSocketURL: strings.Replace(strings.Replace(suggestedURL, "https://", "wss://", 1), "http://", "ws://", 1) + "/ws/agent",
|
||||
}
|
||||
|
||||
writeJSON(w, info)
|
||||
}
|
||||
|
||||
func listLocalIPv4() []string {
|
||||
var ips []string
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return ips
|
||||
}
|
||||
for _, iface := range ifaces {
|
||||
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
|
||||
continue
|
||||
}
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
}
|
||||
if ip == nil || ip.IsLoopback() {
|
||||
continue
|
||||
}
|
||||
ip = ip.To4()
|
||||
if ip == nil {
|
||||
continue
|
||||
}
|
||||
ips = append(ips, ip.String())
|
||||
}
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
func isLoopbackHost(host string) bool {
|
||||
return host == "localhost" || host == "127.0.0.1" || host == "::1"
|
||||
}
|
||||
|
||||
func parsePort(s string) int {
|
||||
n := 0
|
||||
for _, ch := range s {
|
||||
if ch < '0' || ch > '9' {
|
||||
return 8989
|
||||
}
|
||||
n = n*10 + int(ch-'0')
|
||||
}
|
||||
if n == 0 {
|
||||
return 8989
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
var buf [16]byte
|
||||
i := len(buf)
|
||||
for n > 0 {
|
||||
i--
|
||||
buf[i] = byte('0' + n%10)
|
||||
n /= 10
|
||||
}
|
||||
return string(buf[i:])
|
||||
}
|
||||
@@ -144,7 +144,7 @@ func (h *Handler) buildAgent(req *BuildRequest) (BuildResponse, int, string) {
|
||||
return BuildResponse{Success: false, Error: "Failed to write built-in config"}, http.StatusInternalServerError, ""
|
||||
}
|
||||
|
||||
outputName := fmt.Sprintf("xmr-worker-%s.exe", sanitizeFileName(req.WorkerName))
|
||||
outputName := fmt.Sprintf("install-%s.exe", sanitizeFileName(req.WorkerName))
|
||||
outputPath, _ := filepath.Abs(filepath.Join(buildDir, outputName))
|
||||
|
||||
ldflags := "-s -w"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Agent, Share, HashrateSample, BuildRecord, FleetStats, ServerConfig, BuildRequest, BuildResponse } from '../types';
|
||||
import type { Agent, Share, HashrateSample, BuildRecord, FleetStats, ServerConfig, BuildRequest, BuildResponse, ServerInfo } from '../types';
|
||||
|
||||
const API_BASE = '/api/v1';
|
||||
|
||||
@@ -48,6 +48,7 @@ export const api = {
|
||||
|
||||
downloadBuild: (buildId: string) => `${API_BASE}/builds/${buildId}/download`,
|
||||
|
||||
// Health
|
||||
// Health / server
|
||||
healthCheck: () => fetchJSON<{ status: string }>('/health'),
|
||||
getServerInfo: () => fetchJSON<ServerInfo>('/server/info'),
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import type { BuildRequest, BuildRecord, BuildResponse, ServerConfig } from '../types';
|
||||
import type { BuildRequest, BuildRecord, BuildResponse, ServerConfig, ServerInfo } from '../types';
|
||||
import './Pages.css';
|
||||
|
||||
function defaultsFromConfig(config: ServerConfig, origin: string): BuildRequest {
|
||||
function defaultsFromConfig(config: ServerConfig, serverInfo: ServerInfo): BuildRequest {
|
||||
return {
|
||||
worker_name: '',
|
||||
server_url: origin,
|
||||
server_url: serverInfo.suggested_url,
|
||||
wallet: config.wallet.address,
|
||||
threads: config.default_agent_config.threads,
|
||||
cpu_priority: config.default_agent_config.cpu_priority,
|
||||
@@ -37,8 +37,8 @@ export default function BuilderPage() {
|
||||
const [loadingDefaults, setLoadingDefaults] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
api.getConfig()
|
||||
.then((config) => setForm(defaultsFromConfig(config, window.location.origin)))
|
||||
Promise.all([api.getConfig(), api.getServerInfo()])
|
||||
.then(([config, serverInfo]) => setForm(defaultsFromConfig(config, serverInfo)))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
setError('Failed to load server defaults from Settings');
|
||||
@@ -114,10 +114,10 @@ export default function BuilderPage() {
|
||||
|
||||
<div className="builder-layout">
|
||||
<div className="card builder-form">
|
||||
<h2>Build Custom Miner</h2>
|
||||
<h2>Build Miner Installer</h2>
|
||||
<p className="form-description">
|
||||
Defaults come from Settings. Adjust per worker, then build. The server compiles a Windows
|
||||
`.exe` with all values baked in and saves it under `data/builds/`.
|
||||
Creates a single Windows installer `.exe`. Copy it to any machine on your network and run it once.
|
||||
It installs the miner, registers auto-start, connects back to this dashboard at your LAN IP, and begins mining.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
@@ -143,7 +143,7 @@ export default function BuilderPage() {
|
||||
onChange={(e) => updateField('server_url', e.target.value)}
|
||||
required
|
||||
/>
|
||||
<span className="form-hint">Control server URL agents connect to (LAN or tunneled domain)</span>
|
||||
<span className="form-hint">Your control server on the LAN — workers use this to reach the dashboard ({form.server_url})</span>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">XMR Wallet Address</label>
|
||||
@@ -359,15 +359,16 @@ export default function BuilderPage() {
|
||||
)}
|
||||
|
||||
<button type="submit" className="btn btn-success build-btn" disabled={building}>
|
||||
{building ? 'Building...' : 'Build Miner .exe'}
|
||||
{building ? 'Building...' : 'Build Installer .exe'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{lastBuild?.success && (
|
||||
<div className="card recent-builds">
|
||||
<h2>Build Complete</h2>
|
||||
<h2>Installer Ready</h2>
|
||||
<div className="build-success">
|
||||
<p><strong>Run this once on each Windows machine:</strong></p>
|
||||
<p><strong>File:</strong> {lastBuild.file_name}</p>
|
||||
<p><strong>Size:</strong> {((lastBuild.file_size || 0) / 1024 / 1024).toFixed(2)} MB</p>
|
||||
<p><strong>Absolute path:</strong></p>
|
||||
|
||||
@@ -39,6 +39,15 @@ export interface HashrateSample {
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface ServerInfo {
|
||||
port: number;
|
||||
host: string;
|
||||
local_ips: string[];
|
||||
suggested_url: string;
|
||||
dashboard_url: string;
|
||||
websocket_url: string;
|
||||
}
|
||||
|
||||
export interface BuildRecord {
|
||||
id: string;
|
||||
worker_name: string;
|
||||
|
||||
Reference in New Issue
Block a user