feat: docker-compose stacks with Forge-themed UI
Build / build (push) Successful in 10m42s

Adds a new Stacks feature: upload/edit docker-compose YAML,
deploy as atomic units, browse revisions, roll back, and
stream logs. Backend in internal/stack + internal/api/stacks.go,
persistent storage in internal/store/stacks.go.

Stacks pages (list, new, detail) use a modern Forge aesthetic —
Instrument Serif display type, JetBrains Mono for meta/code,
indigo ember accents, dot-grid hero, registration marks on
hover, terminal panel for logs. Palette is sourced from the
app's existing design tokens so the feature remains consistent
with the rest of Tinyforge.

Fonts self-hosted via @fontsource/instrument-serif and
@fontsource/jetbrains-mono to satisfy the strict CSP.
This commit is contained in:
2026-04-16 03:48:37 +03:00
parent b622384774
commit 75424a5f25
23 changed files with 3603 additions and 18 deletions
+28
View File
@@ -15,6 +15,7 @@ import (
"github.com/alexei/tinyforge/internal/events"
"github.com/alexei/tinyforge/internal/npm"
"github.com/alexei/tinyforge/internal/proxy"
"github.com/alexei/tinyforge/internal/stack"
"github.com/alexei/tinyforge/internal/stale"
"github.com/alexei/tinyforge/internal/staticsite"
"github.com/alexei/tinyforge/internal/store"
@@ -44,6 +45,7 @@ type Server struct {
onDNSProviderChanged DNSProviderChangedFunc
staticSiteManager *staticsite.Manager
stackManager *stack.Manager
backupEngine *backup.Engine
dbPath string
shutdownFunc func() // called after restore to trigger graceful shutdown
@@ -90,6 +92,11 @@ func (s *Server) SetStaticSiteManager(mgr *staticsite.Manager) {
s.staticSiteManager = mgr
}
// SetStackManager sets the docker-compose stack manager on the server.
func (s *Server) SetStackManager(mgr *stack.Manager) {
s.stackManager = mgr
}
// SetStaleScanner sets the stale scanner on the server.
// Called after both the API server and scanner are initialized.
func (s *Server) SetStaleScanner(scanner *stale.Scanner) {
@@ -250,6 +257,27 @@ func (s *Server) Router() chi.Router {
r.Post("/volumes/{volId}/upload", s.uploadToVolume)
})
})
// Stacks (docker-compose).
r.Get("/stacks", s.listStacks)
r.Route("/stacks/{id}", func(r chi.Router) {
r.Get("/", s.getStack)
r.Get("/revisions", s.listStackRevisions)
r.Get("/revisions/{revId}", s.getStackRevision)
r.Get("/services", s.getStackServices)
r.Get("/logs", s.getStackLogs)
r.Group(func(r chi.Router) {
r.Use(auth.AdminOnly)
r.Put("/", s.updateStack)
r.Delete("/", s.deleteStack)
r.Post("/revisions", s.createStackRevision)
r.Post("/rollback/{revId}", s.rollbackStack)
r.Post("/stop", s.stopStack)
r.Post("/start", s.startStack)
})
})
r.With(auth.AdminOnly).Post("/stacks", s.createStack)
// Static sites.
r.Get("/sites", s.listStaticSites)
r.Route("/sites/{id}", func(r chi.Router) {