Adding a Web Dashboard to a Windows Console App

한국어로 읽기

TECH INSIGHT / EXPLAINED

A local dashboard can show a console application’s state without becoming another route for accidental control. Start with one read-only JSON endpoint and a clear boundary between snapshots and commands.

Three adult colleagues explain local dashboard
AI-generated teaching illustration, not an actual operating screen or performance record.

Local execution of the actual project

These captures show the existing project UI or functions executed locally with non-private sample inputs. Each caption states the scope of the execution.

The existing BinanceTrading_Futures web dashboard running locally with synthetic prices, indicators and signals. It demonstrates the status view and monitor-only log; the trading engine was not run.
The existing BinanceTrading_Futures web dashboard running locally with synthetic prices, indicators and signals. It demonstrates the status view and monitor-only log; the trading engine was not run.

A snapshot must own its data

HTTP handlers run concurrently. A mutex-protected copy is often enough; an actor goroutine is useful when state transitions already have one owner. Either way, copy maps and slices before returning them. Copying only a struct leaves the underlying arrays shared. If an actor answers through a channel, use a buffered reply channel and select on request cancellation and service shutdown so abandoned clients cannot stall the actor.

Bind locally and fail closed

Bind to 127.0.0.1 by default. Binding to 0.0.0.0 listens on all IPv4 interfaces; internet reachability additionally depends on routing and firewall rules. Never infer “local only” from an empty access key. Reject remote-bind startup when required authentication is missing. Send credentials in an authorization header, not URL parameters that can appear in history and logs. Constant-time comparison is one detail; it does not replace TLS, authorization or exposure control.

Polling needs a failure contract

For example, refresh every three seconds only after the previous request completes, set a request timeout, and show both observation timestamp and connection status. On failure retain the last value with a visible stale marker. A green page with a ten-minute-old balance is misleading. Give the HTTP server read-header, write and idle timeouts appropriate to the endpoints, and shut it down with the application.

A read screen and an action screen have different risks

Use GET for state. Settings, stop and order cancellation require authenticated POST-style actions with server-side permission and input checks; browser cookie sessions also require CSRF defenses. Test that GET cannot trigger them and an absent key yields rejection. For remote viewing, prefer a protected tunnel/VPN or authenticated HTTPS gateway rather than public unauthenticated control routes.

Hiding a window is not process supervision

Console visibility APIs affect a window where supported; they do not create a tray app, service, or restart policy. Keep Windows-specific code in a _windows.go file and account for terminal-host differences. Before adding hide controls, make shutdown and health reporting reliable.

Run a minimal read-only endpoint

Save this as main.go and run go run main.go. Open http://127.0.0.1:9333/api/state to receive {"updated_at":"...Z","running":false}. This standalone demo has no trading engine or command endpoints. If updates are added, they must take the same mutex; production shutdown should use Server.Shutdown.

package main

import (
 "encoding/json"
 "log"
 "net/http"
 "sync"
 "time"
)

type snapshot struct {
 UpdatedAt time.Time `json:"updated_at"`
 Running bool `json:"running"`
}

func readOnlyDashboard() http.Handler {
 var mu sync.RWMutex
 state := snapshot{UpdatedAt: time.Now().UTC(), Running: false}
 mux := http.NewServeMux()
 mux.HandleFunc("/api/state", func(w http.ResponseWriter, r *http.Request) {
  if r.Method != http.MethodGet {
   w.Header().Set("Allow", "GET")
   http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
   return
  }
  mu.RLock()
  copy := state // Only value fields; maps/slices need deeper copies.
  mu.RUnlock()
  w.Header().Set("Content-Type", "application/json; charset=utf-8")
  w.Header().Set("Cache-Control", "no-store")
  if err := json.NewEncoder(w).Encode(copy); err != nil { log.Print(err) }
 })
 return mux
}

func main() {
 srv := &http.Server{
  Addr: "127.0.0.1:9333", Handler: readOnlyDashboard(),
  ReadHeaderTimeout: 3*time.Second, WriteTimeout: 5*time.Second,
  IdleTimeout: 30*time.Second,
 }
 log.Fatal(srv.ListenAndServe())
}

Documentation and next steps

Content and examples reviewed: 2026-09-29

댓글