feat(panel): show build version in user footer

- add configurable user footer visibility
- render the injected release or local-build version

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
bluebluesodaandCommandCodeBot committed 2026-09-08 13:15:58 +00:00
1 parent 9b68d7b7b6
commit a64df2ac96
7 files changed
+64 -3

No files matched your search

+1
View File
@@ -34,6 +34,7 @@ same table; `vps config list` shows the live values with this annotation.
| `panel.session_days` | operator | restart panel | login session lifetime (days) |
| `panel.url_path` | **fixed at install** | — | secret prefix of the user panel; settable only while empty (re-enable) |
| `panel.admin_url_path` | operator | restart panel | secret prefix of the admin panel; an **empty value disables the admin panel** (shown as `disabled`) |
| `panel.show_footer` | operator | restart panel | user panel footer with project link and installed/build version; `false` hides the entire footer |
| `panel.admin_pass_hash` | managed elsewhere | — | bcrypt hash of the admin password; stored in the **DB**, set via `vps admin-passwd` / web UI |
| `net.subnet` | **fixed at install** | — | container subnet `10.<n>.0.0/24`; changing breaks existing containers |
| `net.gateway` | **fixed at install** | — | bridge gateway (derived from subnet) |
+3 -2
View File
@@ -127,7 +127,8 @@ type PanelCfg struct {
// AdminPath is the secret prefix of the admin panel (e.g. /Xy-9ab_cdE),
// a second random path generated at install. Requests that match neither
// URLPath nor AdminPath get a bare, headerless 404.
AdminPath string `yaml:"admin_url_path,omitempty"`
AdminPath string `yaml:"admin_url_path,omitempty"`
ShowFooter bool `yaml:"show_footer"`
}
type NetCfg struct {
@@ -239,7 +240,7 @@ type SnapshotsCfg struct {
func Default() *Config {
c := &Config{}
c.Panel = PanelCfg{Listen: DefaultListen, Cert: DefaultDataDir + "/panel.crt", Key: DefaultDataDir + "/panel.key", DB: DefaultDB, SessionDays: 3}
c.Panel = PanelCfg{Listen: DefaultListen, Cert: DefaultDataDir + "/panel.crt", Key: DefaultDataDir + "/panel.key", DB: DefaultDB, SessionDays: 3, ShowFooter: true}
c.Net = NetCfg{Subnet: DefaultSubnet, Gateway: DefaultGateway, V4Forward: true, Traefik: true, UserPorts: DefaultUserPorts}
c.Incus = IncusCfg{Image: DefaultImage, ImageFallback: DefaultImageFB, Pool: DefaultPool, Bridge: DefaultBridge, Socket: DefaultSocket, SwapRatio: DefaultSwapRatio}
c.Snapshots = SnapshotsCfg{Limit: 1}
+12
View File
@@ -215,6 +215,18 @@ var Fields = []Field{
c.Panel.AdminPath = v
return nil
}},
{"panel.show_footer", KindOperator, ApplyRestart,
"user panel footer with project link and version; false = hide the entire footer",
"true or false",
getStr(func(c *Config) string { return strconv.FormatBool(c.Panel.ShowFooter) }),
func(c *Config, v string) error {
b, ok := parseBool(v)
if !ok {
return fmt.Errorf("panel.show_footer must be true/false or 1/0")
}
c.Panel.ShowFooter = b
return nil
}},
{"panel.admin_pass_hash", KindSpecial, ApplyNone,
"admin password bcrypt hash — set via `vps admin-passwd` (stored in the DB)",
"",
+15
View File
@@ -42,6 +42,9 @@ func TestFieldValueReadsConfig(t *testing.T) {
if v := FieldValue(c, "net.traefik"); v != "true" {
t.Errorf("net.traefik default = %q", v)
}
if v := FieldValue(c, "panel.show_footer"); v != "true" {
t.Errorf("panel.show_footer default = %q", v)
}
}
func TestSnapshotsLimitField(t *testing.T) {
@@ -105,6 +108,17 @@ func TestAssignValidators(t *testing.T) {
if err := FieldFor("net.traefik").Assign(c, "maybe"); err == nil {
t.Error("traefik=maybe accepted")
}
for _, v := range []string{"true", "1", "on", "false", "0", "off"} {
if err := FieldFor("panel.show_footer").Assign(c, v); err != nil {
t.Errorf("show_footer=%q: %v", v, err)
}
}
if err := FieldFor("panel.show_footer").Assign(c, "maybe"); err == nil {
t.Error("show_footer=maybe accepted")
}
if c.Panel.ShowFooter {
t.Error("show_footer should be false after the final test assignment")
}
if err := FieldFor("net.ipv6_subnet").Assign(c, "2001:db8::/64"); err != nil {
t.Fatalf("ipv6_subnet valid: %v", err)
@@ -157,6 +171,7 @@ func TestEditableClassification(t *testing.T) {
"net.ipv6_subnet": "yes",
"panel.url_path": "only-when-empty",
"panel.admin_url_path": "yes",
"panel.show_footer": "yes",
"net.subnet": "no",
"net.gateway": "no",
"incus.pool": "no",
+5
View File
@@ -13,6 +13,7 @@ import (
"vpsmgr/internal/csrf"
"vpsmgr/internal/db"
"vpsmgr/internal/mgr"
"vpsmgr/internal/ver"
)
//go:embed templates/*.html
@@ -170,6 +171,8 @@ type pageData struct {
// default). The overview tints its background and accents with it so an
// impersonating admin can tell users apart. Users cannot set it themselves.
ThemeColor string
ShowFooter bool
Version string
}
func (s *Server) Handler() http.Handler {
@@ -311,6 +314,8 @@ func (s *Server) buildData(u *db.User, msg, errMsg string) pageData {
SSH: "ssh -p " + itoa(u.SSHPort) + " root@" + s.cfg.DisplayIP(),
V4Forward: s.mgr.V4ForwardLive(),
TraefikEnabled: s.mgr.TraefikLive(),
ShowFooter: s.cfg.Panel.ShowFooter,
Version: ver.Version,
InitScript: u.InitScript,
MaxNotesPlaintext: cfg.MaxNotesPlaintextBytes,
QuotaCPU: mgr.FormatCPU(u.CPU),
+27
View File
@@ -184,6 +184,33 @@ func TestUnknownSubpathUnderPrefix(t *testing.T) {
}
}
func TestOverviewFooter(t *testing.T) {
srv, _ := newTestServer(t)
shown := srv.renderToString(t, "overview.html", pageData{
User: &db.User{Name: "alice"},
Prefix: "/" + testSecret,
ShowFooter: true,
Version: "9.8.7-local",
})
for _, want := range []string{
`<footer><a href="https://github.com/bluebluesoda/vpsmgr" target="_blank" rel="noopener">Vpsmgr Lite<br><span>v9.8.7-local</span></a></footer>`,
} {
if !strings.Contains(shown, want) {
t.Errorf("shown footer missing %q", want)
}
}
hidden := srv.renderToString(t, "overview.html", pageData{
User: &db.User{Name: "alice"},
Prefix: "/" + testSecret,
ShowFooter: false,
Version: "9.8.7-local",
})
if strings.Contains(hidden, "<footer>") || strings.Contains(hidden, "Vpsmgr Lite") || strings.Contains(hidden, "9.8.7-local") || strings.Contains(hidden, "https://github.com/bluebluesoda/vpsmgr") {
t.Error("hidden footer should not render any footer content")
}
}
func TestOverviewShowsMonthlyBandwidth(t *testing.T) {
srv, _ := newTestServer(t)
html := srv.renderToString(t, "overview.html", pageData{
+1 -1
View File
@@ -366,7 +366,7 @@ footer a{color:var(--muted);text-decoration:underline}
</div>
<footer><a href="https://github.com/bluebluesoda/vpsmgr" target="_blank" rel="noopener">Vpsmgr lite</a></footer>
{{if .ShowFooter}}<footer><a href="https://github.com/bluebluesoda/vpsmgr" target="_blank" rel="noopener">Vpsmgr Lite<br><span>v{{.Version}}</span></a></footer>{{end}}
<div class="overlay" id="pwModal">
<div class="modal">