feat(docker-watcher): phase 11 - frontend embed & SSE

Embed SvelteKit static build in Go binary via go:embed. Event bus
for pub/sub with deploy log, instance status, and deploy status events.
SSE endpoints for real-time streaming. Frontend SSE client with
exponential backoff reconnection. Makefile for build pipeline.
Update Phase 12 auth plan with OAuth2/OIDC support.
This commit is contained in:
2026-03-27 22:30:25 +03:00
parent d40cf10f88
commit 5558396bb7
16 changed files with 844 additions and 73 deletions
+24 -7
View File
@@ -2,6 +2,7 @@ package main
import (
"context"
"io/fs"
"log"
"net/http"
"os"
@@ -10,11 +11,13 @@ import (
"syscall"
"time"
dockerwatcher "github.com/alexei/docker-watcher"
"github.com/alexei/docker-watcher/internal/api"
"github.com/alexei/docker-watcher/internal/config"
"github.com/alexei/docker-watcher/internal/crypto"
"github.com/alexei/docker-watcher/internal/deployer"
"github.com/alexei/docker-watcher/internal/docker"
"github.com/alexei/docker-watcher/internal/events"
"github.com/alexei/docker-watcher/internal/health"
"github.com/alexei/docker-watcher/internal/notify"
"github.com/alexei/docker-watcher/internal/npm"
@@ -71,14 +74,15 @@ func main() {
// Initialize services.
healthChecker := health.New()
notifier := notify.New()
eventBus := events.New()
dep := deployer.New(dockerClient, npmClient, db, healthChecker, notifier, encKey)
dep := deployer.New(dockerClient, npmClient, db, healthChecker, notifier, eventBus, encKey)
// Initialize webhook handler.
webhookHandler := webhook.NewHandler(db, dep, dockerClient)
// Ensure webhook secret exists.
secret, err := webhook.EnsureWebhookSecret(db)
_, err = webhook.EnsureWebhookSecret(db)
if err != nil {
log.Fatalf("ensure webhook secret: %v", err)
}
@@ -94,16 +98,29 @@ func main() {
}
// Build API server.
apiServer := api.NewServer(db, dockerClient, dep, webhookHandler, encKey)
apiServer := api.NewServer(db, dockerClient, dep, webhookHandler, eventBus, encKey)
router := apiServer.Router()
// Serve embedded static files for the SPA frontend.
// The embed.FS has "web/build" as a prefix, so we sub it to get the root.
webBuildFS, err := fs.Sub(dockerwatcher.WebBuildFS, "web/build")
if err != nil {
log.Printf("WARNING: embedded frontend not available: %v", err)
} else {
staticHandler := api.StaticHandler(webBuildFS)
// Handle all non-API routes with the static file server.
router.NotFound(staticHandler.ServeHTTP)
}
// Start HTTP server.
addr := envOrDefault("LISTEN_ADDR", ":8080")
httpServer := &http.Server{
Addr: addr,
Handler: router,
ReadTimeout: 30 * time.Second,
WriteTimeout: 60 * time.Second,
Addr: addr,
Handler: router,
ReadTimeout: 30 * time.Second,
// WriteTimeout is disabled (0) to support SSE long-lived connections.
// Individual non-SSE handlers should use context timeouts as needed.
WriteTimeout: 0,
IdleTimeout: 120 * time.Second,
}