mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-06-19 09:47:29 +02:00
feat(web): make footer legal links configurable
Add a "main.footer-links" config option so the footer Imprint and Privacy Policy links can point at internal pages (default) or external URLs. External http(s) targets open in a new tab; empty/unset values fall back to the built-in /imprint and /privacy routes, keeping the existing ./var/*.tmpl override mechanism intact. Closes #517 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Entire-Checkpoint: c5dca72c848f
This commit is contained in:
@@ -18,6 +18,10 @@
|
|||||||
"trigger": 180,
|
"trigger": 180,
|
||||||
"resolutions": [240, 60]
|
"resolutions": [240, 60]
|
||||||
},
|
},
|
||||||
|
"footer-links": {
|
||||||
|
"imprint": "/imprint",
|
||||||
|
"privacy": "/privacy"
|
||||||
|
},
|
||||||
"api-subjects": {
|
"api-subjects": {
|
||||||
"subject-job-event": "cc.job.event",
|
"subject-job-event": "cc.job.event",
|
||||||
"subject-node-state": "cc.node.state"
|
"subject-node-state": "cc.node.state"
|
||||||
|
|||||||
@@ -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.
|
# Ensure your logo, and any images you use in your login template has a suitable size.
|
||||||
cp -R img /opt/monitoring/cc-backend/img
|
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.
|
# 5. Copy the systemd service unit file. You may adopt it to your needs.
|
||||||
sudo cp ./init/clustercockpit.service /etc/systemd/system/clustercockpit.service
|
sudo cp ./init/clustercockpit.service /etc/systemd/system/clustercockpit.service
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,18 @@ type ProgramConfig struct {
|
|||||||
|
|
||||||
// Database tuning configuration
|
// Database tuning configuration
|
||||||
DbConfig *DbConfig `json:"db-config"`
|
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 {
|
type DbConfig struct {
|
||||||
@@ -145,6 +157,10 @@ var Keys ProgramConfig = ProgramConfig{
|
|||||||
SessionMaxAge: "168h",
|
SessionMaxAge: "168h",
|
||||||
StopJobsExceedingWalltime: 0,
|
StopJobsExceedingWalltime: 0,
|
||||||
ShortRunningJobsDuration: 5 * 60,
|
ShortRunningJobsDuration: 5 * 60,
|
||||||
|
FooterLinks: FooterLinksConfig{
|
||||||
|
Imprint: "/imprint",
|
||||||
|
Privacy: "/privacy",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func Init(mainConfig json.RawMessage) {
|
func Init(mainConfig json.RawMessage) {
|
||||||
|
|||||||
@@ -133,6 +133,20 @@ var configSchema = `
|
|||||||
},
|
},
|
||||||
"required": ["subject-job-event", "subject-node-state"]
|
"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": {
|
"nodestate-retention": {
|
||||||
"description": "Node state retention configuration for cleaning up old node_state rows.",
|
"description": "Node state retention configuration for cleaning up old node_state rows.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|||||||
@@ -52,8 +52,8 @@
|
|||||||
{{block "footer" .}}
|
{{block "footer" .}}
|
||||||
<footer class="site-footer bg-light">
|
<footer class="site-footer bg-light">
|
||||||
<ul class="footer-list">
|
<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="{{ .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="/privacy" title="Privacy Policy" rel="nofollow">Privacy Policy</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>
|
||||||
<ul class="build-list">
|
<ul class="build-list">
|
||||||
<li class="build-list-item">Version {{ .Build.Version }}</li>
|
<li class="build-list-item">Version {{ .Build.Version }}</li>
|
||||||
|
|||||||
32
web/web.go
32
web/web.go
@@ -74,6 +74,32 @@ type PlotConfiguration struct {
|
|||||||
ColorScheme []string `json:"color-scheme"`
|
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{
|
var UIDefaults = WebConfig{
|
||||||
JobList: JobListConfig{
|
JobList: JobListConfig{
|
||||||
UsePaging: false,
|
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, ...)
|
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
|
Resampling *config.ResampleConfig // If not nil, defines resampling trigger and resolutions
|
||||||
Redirect string // The originally requested URL, for intermediate login handling
|
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) {
|
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)
|
cclog.Debugf("Page config : %v\n", page.Config)
|
||||||
if err := t.Execute(rw, page); err != nil {
|
if err := t.Execute(rw, page); err != nil {
|
||||||
cclog.Errorf("Template error: %s", err.Error())
|
cclog.Errorf("Template error: %s", err.Error())
|
||||||
|
|||||||
Reference in New Issue
Block a user