feat: unified THE FORGE // SECTION headers and merged proxy routes
Build / build (push) Successful in 10m37s
Build / build (push) Successful in 10m37s
UI consistency
- ForgeHero now supports backHref, mono kicker, stats snippet, staggered
entrance animation, and a registration-tick divider
- Every route now opens with the same "THE FORGE // SECTION" eyebrow: projects,
sites, stacks, proxies, events, dns, deploy, settings, stale containers,
site/project detail + env/volumes/browse, new site wizard
- Stacks list/detail/new moved to the shared hero and brand-anchor eyebrow
- Toolbars migrated from bespoke buttons to the shared .forge-btn utilities
- Sidebar footline adds a live UTC "forge clock" and a vim-style g-prefix
quick-nav hint (g d/p/s/k/x/r/e/c jumps to each section)
Proxies page
- Server-side: merge static site proxy routes with instance routes and sort
by domain (internal/api/proxies.go, internal/store/static_sites.go)
- ProxyRoute gains a Source field ("instance" | "static_site")
- Frontend adds source filter tabs and per-source labels/badges
This commit is contained in:
+20
-3
@@ -3,10 +3,12 @@ package api
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// listProxyRoutes handles GET /api/proxies.
|
||||
// Returns all proxy-enabled instances with project and stage names.
|
||||
// Returns proxy routes from both Docker instances and static sites,
|
||||
// merged and sorted by domain.
|
||||
func (s *Server) listProxyRoutes(w http.ResponseWriter, r *http.Request) {
|
||||
settings, err := s.store.GetSettings()
|
||||
if err != nil {
|
||||
@@ -15,12 +17,27 @@ func (s *Server) listProxyRoutes(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
routes, err := s.store.ListProxyRoutes(settings.Domain)
|
||||
instanceRoutes, err := s.store.ListProxyRoutes(settings.Domain)
|
||||
if err != nil {
|
||||
slog.Error("failed to list proxy routes", "error", err)
|
||||
slog.Error("failed to list instance proxy routes", "error", err)
|
||||
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
|
||||
siteRoutes, err := s.store.ListStaticSiteProxyRoutes(settings.Domain)
|
||||
if err != nil {
|
||||
slog.Error("failed to list static site proxy routes", "error", err)
|
||||
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
|
||||
routes := append(instanceRoutes, siteRoutes...)
|
||||
sort.SliceStable(routes, func(i, j int) bool {
|
||||
if routes[i].Domain == routes[j].Domain {
|
||||
return routes[i].ProjectName < routes[j].ProjectName
|
||||
}
|
||||
return routes[i].Domain < routes[j].Domain
|
||||
})
|
||||
|
||||
respondJSON(w, http.StatusOK, routes)
|
||||
}
|
||||
|
||||
@@ -119,8 +119,10 @@ func (s *Store) ListAllInstances() ([]Instance, error) {
|
||||
return instances, rows.Err()
|
||||
}
|
||||
|
||||
// ProxyRoute represents a proxy-enabled instance with project and stage names.
|
||||
// ProxyRoute represents a proxy-enabled resource (Docker instance or static site)
|
||||
// joined with the human-readable names needed to render the Proxies page.
|
||||
type ProxyRoute struct {
|
||||
Source string `json:"source"` // "instance" or "static_site"
|
||||
InstanceID string `json:"instance_id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
ProjectName string `json:"project_name"`
|
||||
@@ -164,6 +166,7 @@ func (s *Store) ListProxyRoutes(domain string) ([]ProxyRoute, error) {
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan proxy route: %w", err)
|
||||
}
|
||||
r.Source = "instance"
|
||||
if domain != "" && r.Subdomain != "" {
|
||||
r.Domain = r.Subdomain + "." + domain
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@@ -154,6 +155,50 @@ func (s *Store) UpdateStaticSiteContainer(id, containerID, proxyRouteID string)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListStaticSiteProxyRoutes returns proxy routes backed by static sites,
|
||||
// shaped to match the unified ProxyRoute model used by the Proxies page.
|
||||
// Sites without an active proxy route are skipped.
|
||||
func (s *Store) ListStaticSiteProxyRoutes(domain string) ([]ProxyRoute, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT id, name, mode, provider, domain, container_id, proxy_route_id, status, created_at
|
||||
FROM static_sites
|
||||
WHERE proxy_route_id != ''
|
||||
ORDER BY name`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query static site proxy routes: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
suffix := ""
|
||||
if domain != "" {
|
||||
suffix = "." + strings.ToLower(domain)
|
||||
}
|
||||
|
||||
routes := []ProxyRoute{}
|
||||
for rows.Next() {
|
||||
var r ProxyRoute
|
||||
var mode, provider, fullDomain string
|
||||
if err := rows.Scan(
|
||||
&r.InstanceID, &r.ProjectName, &mode, &provider, &fullDomain,
|
||||
&r.ContainerID, &r.ProxyRouteID, &r.Status, &r.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan static site proxy route: %w", err)
|
||||
}
|
||||
r.Source = "static_site"
|
||||
r.StageName = mode
|
||||
r.ImageTag = provider
|
||||
r.Domain = fullDomain
|
||||
if suffix != "" && strings.HasSuffix(strings.ToLower(fullDomain), suffix) {
|
||||
r.Subdomain = fullDomain[:len(fullDomain)-len(suffix)]
|
||||
} else {
|
||||
r.Subdomain = fullDomain
|
||||
}
|
||||
routes = append(routes, r)
|
||||
}
|
||||
return routes, rows.Err()
|
||||
}
|
||||
|
||||
// DeleteStaticSite removes a static site by ID. Cascading deletes handle secrets.
|
||||
func (s *Store) DeleteStaticSite(id string) error {
|
||||
result, err := s.db.Exec(`DELETE FROM static_sites WHERE id = ?`, id)
|
||||
|
||||
Reference in New Issue
Block a user