binwiederhier 1 месяц назад
Родитель
Сommit
856f150958
9 измененных файлов с 117 добавлено и 91 удалено
  1. 24 1
      cmd/serve.go
  2. 16 3
      main.go
  3. 20 9
      server/config.go
  4. 21 21
      server/server.go
  5. 3 3
      server/server_twilio.go
  6. 0 5
      server/types.go
  7. 5 1
      web/src/app/Pruner.js
  8. 15 35
      web/src/app/VersionChecker.js
  9. 13 13
      web/src/components/Navigation.jsx

+ 24 - 1
cmd/serve.go

@@ -128,6 +128,12 @@ Examples:
   ntfy serve --listen-http :8080  # Starts server with alternate port`,
 }
 
+// App metadata fields used to pass from
+const (
+	MetadataKeyCommit = "commit"
+	MetadataKeyDate   = "date"
+)
+
 func execServe(c *cli.Context) error {
 	if c.NArg() > 0 {
 		return errors.New("no arguments expected, see 'ntfy serve --help' for help")
@@ -501,7 +507,9 @@ func execServe(c *cli.Context) error {
 	conf.WebPushStartupQueries = webPushStartupQueries
 	conf.WebPushExpiryDuration = webPushExpiryDuration
 	conf.WebPushExpiryWarningDuration = webPushExpiryWarningDuration
-	conf.Version = c.App.Version
+	conf.BuildVersion = c.App.Version
+	conf.BuildDate = maybeFromMetadata(c.App.Metadata, MetadataKeyDate)
+	conf.BuildCommit = maybeFromMetadata(c.App.Metadata, MetadataKeyCommit)
 
 	// Check if we should run as a Windows service
 	if ranAsService, err := maybeRunAsService(conf); err != nil {
@@ -655,3 +663,18 @@ func parseTokens(users []*user.User, tokensRaw []string) (map[string][]*user.Tok
 	}
 	return tokens, nil
 }
+
+func maybeFromMetadata(m map[string]any, key string) string {
+	if m == nil {
+		return ""
+	}
+	v, exists := m[key]
+	if !exists {
+		return ""
+	}
+	s, ok := v.(string)
+	if !ok {
+		return ""
+	}
+	return s
+}

+ 16 - 3
main.go

@@ -2,12 +2,14 @@ package main
 
 import (
 	"fmt"
-	"github.com/urfave/cli/v2"
-	"heckel.io/ntfy/v2/cmd"
 	"os"
 	"runtime"
+
+	"github.com/urfave/cli/v2"
+	"heckel.io/ntfy/v2/cmd"
 )
 
+// These variables are set during build time using -ldflags
 var (
 	version = "dev"
 	commit  = "unknown"
@@ -24,13 +26,24 @@ the Matrix room (https://matrix.to/#/#ntfy:matrix.org).
 
 ntfy %s (%s), runtime %s, built at %s
 Copyright (C) Philipp C. Heckel, licensed under Apache License 2.0 & GPLv2
-`, version, commit[:7], runtime.Version(), date)
+`, version, maybeShortCommit(commit), runtime.Version(), date)
 
 	app := cmd.New()
 	app.Version = version
+	app.Metadata = map[string]any{
+		cmd.MetadataKeyDate:   date,
+		cmd.MetadataKeyCommit: commit,
+	}
 
 	if err := app.Run(os.Args); err != nil {
 		fmt.Fprintln(os.Stderr, err.Error())
 		os.Exit(1)
 	}
 }
+
+func maybeShortCommit(commit string) string {
+	if len(commit) > 7 {
+		return commit[:7]
+	}
+	return commit
+}

+ 20 - 9
server/config.go

@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"io/fs"
 	"net/netip"
+	"reflect"
 	"text/template"
 	"time"
 
@@ -182,7 +183,9 @@ type Config struct {
 	WebPushStartupQueries                string
 	WebPushExpiryDuration                time.Duration
 	WebPushExpiryWarningDuration         time.Duration
-	Version                              string // Injected by App
+	BuildVersion                         string // Injected by App
+	BuildDate                            string // Injected by App
+	BuildCommit                          string // Injected by App
 }
 
 // NewConfig instantiates a default new server config
@@ -269,23 +272,31 @@ func NewConfig() *Config {
 		EnableReservations:                   false,
 		RequireLogin:                         false,
 		AccessControlAllowOrigin:             "*",
-		Version:                              "",
 		WebPushPrivateKey:                    "",
 		WebPushPublicKey:                     "",
 		WebPushFile:                          "",
 		WebPushEmailAddress:                  "",
 		WebPushExpiryDuration:                DefaultWebPushExpiryDuration,
 		WebPushExpiryWarningDuration:         DefaultWebPushExpiryWarningDuration,
+		BuildVersion:                         "",
+		BuildDate:                            "",
 	}
 }
 
-// Hash computes a SHA-256 hash of the configuration. This is used to detect
-// configuration changes for the web app version check feature.
+// Hash computes an SHA-256 hash of the configuration. This is used to detect
+// configuration changes for the web app version check feature. It uses reflection
+// to include all JSON-serializable fields automatically.
 func (c *Config) Hash() string {
-	b, err := json.Marshal(c)
-	if err != nil {
-		fmt.Println(err)
+	v := reflect.ValueOf(*c)
+	t := v.Type()
+	var result string
+	for i := 0; i < v.NumField(); i++ {
+		field := v.Field(i)
+		fieldName := t.Field(i).Name
+		// Try to marshal the field and skip if it fails (e.g. *template.Template, netip.Prefix)
+		if b, err := json.Marshal(field.Interface()); err == nil {
+			result += fmt.Sprintf("%s:%s|", fieldName, string(b))
+		}
 	}
-	fmt.Println(string(b))
-	return fmt.Sprintf("%x", sha256.Sum256(b))
+	return fmt.Sprintf("%x", sha256.Sum256([]byte(result)))
 }

+ 21 - 21
server/server.go

@@ -90,7 +90,7 @@ var (
 	matrixPushPath                                       = "/_matrix/push/v1/notify"
 	metricsPath                                          = "/metrics"
 	apiHealthPath                                        = "/v1/health"
-	apiVersionPath                                       = "/v1/version"
+	apiConfigPath                                        = "/v1/config"
 	apiStatsPath                                         = "/v1/stats"
 	apiWebPushPath                                       = "/v1/webpush"
 	apiTiersPath                                         = "/v1/tiers"
@@ -278,9 +278,9 @@ func (s *Server) Run() error {
 	if s.config.ProfileListenHTTP != "" {
 		listenStr += fmt.Sprintf(" %s[http/profile]", s.config.ProfileListenHTTP)
 	}
-	log.Tag(tagStartup).Info("Listening on%s, ntfy %s, log level is %s", listenStr, s.config.Version, log.CurrentLevel().String())
+	log.Tag(tagStartup).Info("Listening on%s, ntfy %s, log level is %s", listenStr, s.config.BuildVersion, log.CurrentLevel().String())
 	if log.IsFile() {
-		fmt.Fprintf(os.Stderr, "Listening on%s, ntfy %s\n", listenStr, s.config.Version)
+		fmt.Fprintf(os.Stderr, "Listening on%s, ntfy %s\n", listenStr, s.config.BuildVersion)
 		fmt.Fprintf(os.Stderr, "Logs are written to %s\n", log.File())
 	}
 	mux := http.NewServeMux()
@@ -461,8 +461,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
 		return s.ensureWebEnabled(s.handleEmpty)(w, r, v)
 	} else if r.Method == http.MethodGet && r.URL.Path == apiHealthPath {
 		return s.handleHealth(w, r, v)
-	} else if r.Method == http.MethodGet && r.URL.Path == apiVersionPath {
-		return s.handleVersion(w, r, v)
+	} else if r.Method == http.MethodGet && r.URL.Path == apiConfigPath {
+		return s.handleConfig(w, r, v)
 	} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath {
 		return s.ensureWebEnabled(s.handleWebConfig)(w, r, v)
 	} else if r.Method == http.MethodGet && r.URL.Path == webManifestPath {
@@ -603,16 +603,24 @@ func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request, _ *visitor
 	return s.writeJSON(w, response)
 }
 
-func (s *Server) handleVersion(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
-	response := &apiVersionResponse{
-		Version:    s.config.Version,
-		ConfigHash: s.config.Hash(),
-	}
-	return s.writeJSON(w, response)
+func (s *Server) handleConfig(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
+	w.Header().Set("Cache-Control", "no-cache")
+	return s.writeJSON(w, s.configResponse())
 }
 
 func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
-	response := &apiConfigResponse{
+	b, err := json.MarshalIndent(s.configResponse(), "", "  ")
+	if err != nil {
+		return err
+	}
+	w.Header().Set("Content-Type", "text/javascript")
+	w.Header().Set("Cache-Control", "no-cache")
+	_, err = io.WriteString(w, fmt.Sprintf("// Generated server configuration\nvar config = %s;\n", string(b)))
+	return err
+}
+
+func (s *Server) configResponse() *apiConfigResponse {
+	return &apiConfigResponse{
 		BaseURL:            "", // Will translate to window.location.origin
 		AppRoot:            s.config.WebRoot,
 		EnableLogin:        s.config.EnableLogin,
@@ -628,14 +636,6 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
 		DisallowedTopics:   s.config.DisallowedTopics,
 		ConfigHash:         s.config.Hash(),
 	}
-	b, err := json.MarshalIndent(response, "", "  ")
-	if err != nil {
-		return err
-	}
-	w.Header().Set("Content-Type", "text/javascript")
-	w.Header().Set("Cache-Control", "no-cache")
-	_, err = io.WriteString(w, fmt.Sprintf("// Generated server configuration\nvar config = %s;\n", string(b)))
-	return err
 }
 
 // handleWebManifest serves the web app manifest for the progressive web app (PWA)
@@ -1003,7 +1003,7 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) {
 		logvm(v, m).Err(err).Warn("Unable to publish poll request")
 		return
 	}
-	req.Header.Set("User-Agent", "ntfy/"+s.config.Version)
+	req.Header.Set("User-Agent", "ntfy/"+s.config.BuildVersion)
 	req.Header.Set("X-Poll-ID", m.ID)
 	if s.config.UpstreamAccessToken != "" {
 		req.Header.Set("Authorization", util.BearerAuth(s.config.UpstreamAccessToken))

+ 3 - 3
server/server_twilio.go

@@ -125,7 +125,7 @@ func (s *Server) callPhoneInternal(data url.Values) (string, error) {
 	if err != nil {
 		return "", err
 	}
-	req.Header.Set("User-Agent", "ntfy/"+s.config.Version)
+	req.Header.Set("User-Agent", "ntfy/"+s.config.BuildVersion)
 	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
 	req.Header.Set("Authorization", util.BasicAuth(s.config.TwilioAccount, s.config.TwilioAuthToken))
 	resp, err := http.DefaultClient.Do(req)
@@ -149,7 +149,7 @@ func (s *Server) verifyPhoneNumber(v *visitor, r *http.Request, phoneNumber, cha
 	if err != nil {
 		return err
 	}
-	req.Header.Set("User-Agent", "ntfy/"+s.config.Version)
+	req.Header.Set("User-Agent", "ntfy/"+s.config.BuildVersion)
 	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
 	req.Header.Set("Authorization", util.BasicAuth(s.config.TwilioAccount, s.config.TwilioAuthToken))
 	resp, err := http.DefaultClient.Do(req)
@@ -175,7 +175,7 @@ func (s *Server) verifyPhoneNumberCheck(v *visitor, r *http.Request, phoneNumber
 	if err != nil {
 		return err
 	}
-	req.Header.Set("User-Agent", "ntfy/"+s.config.Version)
+	req.Header.Set("User-Agent", "ntfy/"+s.config.BuildVersion)
 	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
 	req.Header.Set("Authorization", util.BasicAuth(s.config.TwilioAccount, s.config.TwilioAuthToken))
 	resp, err := http.DefaultClient.Do(req)

+ 0 - 5
server/types.go

@@ -317,11 +317,6 @@ type apiHealthResponse struct {
 	Healthy bool `json:"healthy"`
 }
 
-type apiVersionResponse struct {
-	Version    string `json:"version"`
-	ConfigHash string `json:"config_hash"`
-}
-
 type apiStatsResponse struct {
 	Messages     int64   `json:"messages"`
 	MessagesRate float64 `json:"messages_rate"` // Average number of messages per second

+ 5 - 1
web/src/app/Pruner.js

@@ -19,7 +19,11 @@ class Pruner {
   }
 
   stopWorker() {
-    clearTimeout(this.timer);
+    if (this.timer) {
+      clearTimeout(this.timer);
+      this.timer = null;
+    }
+    console.log("[VersionChecker] Stopped pruner checker");
   }
 
   async prune() {

+ 15 - 35
web/src/app/VersionChecker.js

@@ -1,5 +1,5 @@
 /**
- * VersionChecker polls the /v1/version endpoint to detect server restarts
+ * VersionChecker polls the /v1/config endpoint to detect new server versions
  * or configuration changes, prompting users to refresh the page.
  */
 
@@ -9,7 +9,8 @@ class VersionChecker {
   constructor() {
     this.initialConfigHash = null;
     this.listener = null;
-    this.intervalId = null;
+    console.log("XXXXXXxxxx set listener null");
+    this.timer = null;
   }
 
   /**
@@ -18,73 +19,52 @@ class VersionChecker {
    */
   startWorker() {
     // Store initial config hash from the config loaded at page load
-    this.initialConfigHash = window.config?.config_hash || null;
-
-    if (!this.initialConfigHash) {
-      console.log("[VersionChecker] No initial config_hash found, version checking disabled");
-      return;
-    }
-
-    console.log("[VersionChecker] Starting version checker with initial hash:", this.initialConfigHash);
-
-    // Start polling
-    this.intervalId = setInterval(() => this.checkVersion(), CHECK_INTERVAL);
+    this.initialConfigHash = window.config?.config_hash || "";
+    console.log("[VersionChecker] Starting version checker");
+    this.timer = setInterval(() => this.checkVersion(), CHECK_INTERVAL);
   }
 
-  /**
-   * Stops the version checker worker.
-   */
   stopWorker() {
-    if (this.intervalId) {
-      clearInterval(this.intervalId);
-      this.intervalId = null;
+    if (this.timer) {
+      clearInterval(this.timer);
+      this.timer = null;
     }
     console.log("[VersionChecker] Stopped version checker");
   }
 
-  /**
-   * Registers a listener that will be called when a version change is detected.
-   * @param {function} listener - Callback function that receives no arguments
-   */
   registerListener(listener) {
     this.listener = listener;
   }
 
-  /**
-   * Resets the listener.
-   */
   resetListener() {
     this.listener = null;
   }
 
-  /**
-   * Fetches the current version from the server and compares it with the initial config hash.
-   */
   async checkVersion() {
     if (!this.initialConfigHash) {
       return;
     }
 
     try {
-      const response = await fetch(`${window.config?.base_url || ""}/v1/version`);
+      const response = await fetch(`${window.config?.base_url || ""}/v1/config`);
       if (!response.ok) {
-        console.log("[VersionChecker] Failed to fetch version:", response.status);
+        console.log("[VersionChecker] Failed to fetch config:", response.status);
         return;
       }
 
       const data = await response.json();
       const currentHash = data.config_hash;
 
-      console.log("[VersionChecker] Checked version, initial:", this.initialConfigHash, "current:", currentHash);
-
       if (currentHash && currentHash !== this.initialConfigHash) {
-        console.log("[VersionChecker] Config hash changed, notifying listener");
+        console.log("[VersionChecker] Version or config changed, showing banner");
         if (this.listener) {
           this.listener();
         }
+      } else {
+        console.log("[VersionChecker] No version change detected");
       }
     } catch (error) {
-      console.log("[VersionChecker] Error checking version:", error);
+      console.log("[VersionChecker] Error checking config:", error);
     }
   }
 }

+ 13 - 13
web/src/components/Navigation.jsx

@@ -1,27 +1,27 @@
 import {
-  Drawer,
-  ListItemButton,
-  ListItemIcon,
-  ListItemText,
-  Toolbar,
-  Divider,
-  List,
   Alert,
   AlertTitle,
   Badge,
+  Box,
+  Button,
   CircularProgress,
+  Divider,
+  Drawer,
+  IconButton,
   Link,
+  List,
+  ListItemButton,
+  ListItemIcon,
+  ListItemText,
   ListSubheader,
   Portal,
+  Toolbar,
   Tooltip,
   Typography,
-  Box,
-  IconButton,
-  Button,
   useTheme,
 } from "@mui/material";
 import * as React from "react";
-import { useCallback, useContext, useState } from "react";
+import { useContext, useState } from "react";
 import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline";
 import Person from "@mui/icons-material/Person";
 import SettingsIcon from "@mui/icons-material/Settings";
@@ -93,9 +93,9 @@ const NavList = (props) => {
   const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false);
   const [versionChanged, setVersionChanged] = useState(false);
 
-  const handleVersionChange = useCallback(() => {
+  const handleVersionChange = () => {
     setVersionChanged(true);
-  }, []);
+  };
 
   useVersionChangeListener(handleVersionChange);