Merge pull request #562 from ClusterCockpit/feature/517-make-legal-links-configurable

feat(web): make footer legal links configurable
This commit is contained in:
Jan Eitzinger
2026-06-17 18:35:25 +02:00
committed by GitHub
6 changed files with 75 additions and 2 deletions

View File

@@ -18,6 +18,10 @@
"trigger": 180,
"resolutions": [240, 60]
},
"footer-links": {
"imprint": "/imprint",
"privacy": "/privacy"
},
"api-subjects": {
"subject-job-event": "cc.job.event",
"subject-node-state": "cc.node.state"

View File

@@ -37,6 +37,13 @@ cp privacy.tmpl /opt/monitoring/cc-backend/var/
# Ensure your logo, and any images you use in your login template has a suitable size.
cp -R img /opt/monitoring/cc-backend/img
# 4b. (Optional) Instead of overriding imprint.tmpl/privacy.tmpl, you can point the
# footer links to external pages via "main.footer-links" in config.json:
# "footer-links": { "imprint": "https://example.com/imprint", "privacy": "https://example.com/privacy" }
# Values may be internal paths (default "/imprint", "/privacy") or external URLs;
# external URLs (http/https) open in a new browser tab. An empty value falls back
# to the internal page.
# 5. Copy the systemd service unit file. You may adopt it to your needs.
sudo cp ./init/clustercockpit.service /etc/systemd/system/clustercockpit.service

View File

@@ -80,6 +80,18 @@ type ProgramConfig struct {
// Database tuning configuration
DbConfig *DbConfig `json:"db-config"`
// Optional external/legal links shown in the footer.
FooterLinks FooterLinksConfig `json:"footer-links"`
}
// FooterLinksConfig configures the legal/footer links rendered in the UI.
// Each value may be an internal path (e.g. "/imprint") or an external URL.
type FooterLinksConfig struct {
// Target URL/path for the "Imprint" footer entry.
Imprint string `json:"imprint"`
// Target URL/path for the "Privacy Policy" footer entry.
Privacy string `json:"privacy"`
}
type DbConfig struct {
@@ -145,6 +157,10 @@ var Keys ProgramConfig = ProgramConfig{
SessionMaxAge: "168h",
StopJobsExceedingWalltime: 0,
ShortRunningJobsDuration: 5 * 60,
FooterLinks: FooterLinksConfig{
Imprint: "/imprint",
Privacy: "/privacy",
},
}
func Init(mainConfig json.RawMessage) {

View File

@@ -133,6 +133,20 @@ var configSchema = `
},
"required": ["subject-job-event", "subject-node-state"]
},
"footer-links": {
"description": "Optional footer links for legal pages (imprint/privacy). Each value may be an internal path or an external URL.",
"type": "object",
"properties": {
"imprint": {
"description": "Target URL/path for the footer imprint link.",
"type": "string"
},
"privacy": {
"description": "Target URL/path for the footer privacy link.",
"type": "string"
}
}
},
"nodestate-retention": {
"description": "Node state retention configuration for cleaning up old node_state rows.",
"type": "object",

View File

@@ -52,8 +52,8 @@
{{block "footer" .}}
<footer class="site-footer bg-light">
<ul class="footer-list">
<li class="footer-list-item"><a class="link-secondary fs-5" href="/imprint" title="Imprint" rel="nofollow">Imprint</a></li>
<li class="footer-list-item"><a class="link-secondary fs-5" href="/privacy" title="Privacy Policy" rel="nofollow">Privacy Policy</a></li>
<li class="footer-list-item"><a class="link-secondary fs-5" href="{{ .FooterLinks.Imprint.URL }}" title="Imprint"{{ if .FooterLinks.Imprint.External }} target="_blank" rel="noopener noreferrer"{{ else }} rel="nofollow"{{ end }}>Imprint</a></li>
<li class="footer-list-item"><a class="link-secondary fs-5" href="{{ .FooterLinks.Privacy.URL }}" title="Privacy Policy"{{ if .FooterLinks.Privacy.External }} target="_blank" rel="noopener noreferrer"{{ else }} rel="nofollow"{{ end }}>Privacy Policy</a></li>
</ul>
<ul class="build-list">
<li class="build-list-item">Version {{ .Build.Version }}</li>

View File

@@ -74,6 +74,32 @@ type PlotConfiguration struct {
ColorScheme []string `json:"color-scheme"`
}
const (
defaultImprintLink = "/imprint"
defaultPrivacyLink = "/privacy"
)
// FooterLink is the render-time representation of a single footer legal link.
type FooterLink struct {
URL string // Resolved target: internal path or external URL.
External bool // True if the target is an external URL (opened in a new tab).
}
// FooterLinks holds the resolved legal links shown in the site footer.
type FooterLinks struct {
Imprint FooterLink
Privacy FooterLink
}
// resolveFooterLink falls back to def when v is empty and flags external URLs.
func resolveFooterLink(v, def string) FooterLink {
if v == "" {
v = def
}
external := strings.HasPrefix(v, "http://") || strings.HasPrefix(v, "https://")
return FooterLink{URL: v, External: external}
}
var UIDefaults = WebConfig{
JobList: JobListConfig{
UsePaging: false,
@@ -266,6 +292,7 @@ type Page struct {
Config map[string]any // UI settings for the currently logged in user (e.g. line width, ...)
Resampling *config.ResampleConfig // If not nil, defines resampling trigger and resolutions
Redirect string // The originally requested URL, for intermediate login handling
FooterLinks FooterLinks // Resolved legal links for the site footer
}
func RenderTemplate(rw http.ResponseWriter, file string, page *Page) {
@@ -289,6 +316,11 @@ func RenderTemplate(rw http.ResponseWriter, file string, page *Page) {
}
}
page.FooterLinks = FooterLinks{
Imprint: resolveFooterLink(config.Keys.FooterLinks.Imprint, defaultImprintLink),
Privacy: resolveFooterLink(config.Keys.FooterLinks.Privacy, defaultPrivacyLink),
}
cclog.Debugf("Page config : %v\n", page.Config)
if err := t.Execute(rw, page); err != nil {
cclog.Errorf("Template error: %s", err.Error())