feat: CI + Nix deploy + prod sectors-hackaton.asepharyana.my.id
CI / go test + web typecheck (push) Canceled after 0s
CI / build + deploy (Nix) — flowsight (push) Canceled after 0s

- Backend serve web dist (WEB_DIST_DIR + SPA fallback, /api 404 JSON)
- flake.nix: single flowsight derivation (go 1.25 + pnpm build)
- deploy/flowsight.service: systemd on :4022 via bws-exec
- .github/workflows/ci.yml: go test + tsc gate, nix build, attic, SSH deploy
This commit is contained in:
asepharyana
2026-09-15 14:45:23 +07:00
parent 8c184ccae1
commit 21e8568108
8 changed files with 378 additions and 0 deletions
+3
View File
@@ -90,6 +90,9 @@ func (s *Server) Router() http.Handler {
r.Get("/accuracy", s.Accuracy)
r.Post("/chat", s.Chat)
})
if s.Cfg.StaticDir != "" {
r.NotFound(s.spaHandler())
}
return r
}
+39
View File
@@ -0,0 +1,39 @@
// Package api serves the FlowSight REST API (docs/API.md) on chi.
package api
import (
"net/http"
"os"
"path/filepath"
"strings"
)
// spaHandler serves the prebuilt web dist (WEB_DIST_DIR) with an SPA fallback:
// existing files (assets, index.html) are served directly, unknown paths fall
// back to index.html so client-side routes (/routines, /report/:ticker, ...)
// resolve. Only registered when StaticDir is set; API-only mode otherwise.
func (s *Server) spaHandler() http.HandlerFunc {
root := s.Cfg.StaticDir
fileSrv := http.FileServer(http.Dir(root))
index := filepath.Join(root, "index.html")
return func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/api/") || r.URL.Path == "/api" {
writeErr(w, http.StatusNotFound, "not found")
return
}
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
http.ServeFile(w, r, index)
return
}
clean := filepath.Clean(strings.TrimPrefix(r.URL.Path, "/"))
if strings.Contains(clean, "..") {
http.NotFound(w, r)
return
}
if fi, err := os.Stat(filepath.Join(root, clean)); err == nil && !fi.IsDir() {
fileSrv.ServeHTTP(w, r)
return
}
http.ServeFile(w, r, index)
}
}
+3
View File
@@ -27,6 +27,8 @@ type Config struct {
DiscordWebhookURL string
CreditCapPerCycle int
Watchlist []string
// StaticDir serves the prebuilt web dist (WEB_DIST_DIR). Empty = API only.
StaticDir string
}
// HasSectorsKey reports whether live Sectors API calls are possible.
@@ -83,5 +85,6 @@ func Load() Config {
DiscordWebhookURL: os.Getenv("DISCORD_WEBHOOK_URL"),
CreditCapPerCycle: getenvInt("CREDIT_CAP_PER_CYCLE", 120),
Watchlist: tickers,
StaticDir: os.Getenv("WEB_DIST_DIR"),
}
}