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:
1 parent
9b68d7b7b6
commit
a64df2ac96
7 files changed
+64
-3
No files matched your search
@@ -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) |
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)",
|
||||
"",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user