added about-section and update-notification
authorPhiTux <redacted>
Wed, 10 Sep 2025 23:02:47 +0000 (01:02 +0200)
committerPhiTux <redacted>
Wed, 10 Sep 2025 23:02:47 +0000 (01:02 +0200)
backend/handlers/additional.go
backend/handlers/users.go
backend/main.go
backend/utils/helpers.go
frontend/src/routes/(authed)/+layout.svelte

index 9d50c12d601d3f004dffc3187fd7a3cb4288985c..3ff9e5aeb2b8b3ed0888a509c278a2052fcb1999 100644 (file)
@@ -2770,3 +2770,14 @@ func GetStatistics(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(dayStats)
 }
+
+// GetVersionInfo returns the current application version (public endpoint, no auth required)
+func GetVersionInfo(w http.ResponseWriter, r *http.Request) {
+       latest_stable, latest_overall := utils.GetLatestVersion()
+
+       utils.JSONResponse(w, http.StatusOK, map[string]string{
+               "current_version":        utils.GetVersion(),
+               "latest_stable_version":  latest_stable,
+               "latest_overall_version": latest_overall,
+       })
+}
index c35bbbe8645e82f6ce97c94385b6518097956b73..3b98b20b78c4e00e85b2942e451d328f09d65aed 100644 (file)
@@ -437,6 +437,8 @@ func GetDefaultSettings() map[string]any {
                "useDarkMode":                false,
                "background":                 "gradient",
                "monochromeBackgroundColor":  "#ececec",
+               "checkForUpdates":            true,
+               "includeTestVersions":        false,
        }
 }
 
index 5ffeabab331feb2871fe3310e3ea7153da2909b4..c7aae836f311e4407362a3b48e759f4773e48c3b 100644 (file)
@@ -14,6 +14,9 @@ import (
        "github.com/phitux/dailytxt/backend/utils"
 )
 
+// Application version - UPDATE THIS FOR NEW RELEASES
+const AppVersion = "2.0.0-testing.1"
+
 // longTimeoutEndpoints defines endpoints that need extended timeouts
 var longTimeoutEndpoints = map[string]bool{
        "/logs/uploadFile":   true,
@@ -42,6 +45,10 @@ func main() {
        logger := log.New(os.Stdout, "dailytxt: ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
        logger.Println("Server starting...")
 
+       // Set application version
+       utils.SetVersion(AppVersion)
+       logger.Printf("DailyTxT version: %s", AppVersion)
+
        // Load settings
        if err := utils.InitSettings(); err != nil {
                logger.Fatalf("Failed to initialize settings: %v", err)
@@ -53,6 +60,9 @@ func main() {
        // Create a new router
        mux := http.NewServeMux()
 
+       // Public routes (no authentication required)
+       mux.HandleFunc("GET /version", handlers.GetVersionInfo)
+
        // Register routes
        mux.HandleFunc("POST /users/login", handlers.Login)
        mux.HandleFunc("GET /users/migrationProgress", handlers.GetMigrationProgress)
index c13402b3715ce89f8e35057f9399635cc3fe2c0b..c3a020fc8160e64a2c4d8653700db782eb7c7feb 100644 (file)
@@ -7,12 +7,17 @@ import (
        "log"
        "net/http"
        "os"
+       "strconv"
        "strings"
+       "time"
 )
 
 // Global logger
 var Logger *log.Logger
 
+// Application version (separate from AppSettings)
+var AppVersion string
+
 func init() {
        // Initialize logger
        Logger = log.New(os.Stdout, "dailytxt: ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
@@ -42,6 +47,16 @@ type AppSettings struct {
 // Global settings
 var Settings AppSettings
 
+// SetVersion sets the application version
+func SetVersion(version string) {
+       AppVersion = version
+}
+
+// GetVersion returns the current application version
+func GetVersion() string {
+       return AppVersion
+}
+
 // InitSettings loads the application settings
 func InitSettings() error {
        // Default settings
@@ -406,3 +421,184 @@ func GetUsernameByID(userID int) string {
        fmt.Printf("user not found with ID: %d\n", userID)
        return ""
 }
+
+// Docker Hub API structures
+type DockerHubTag struct {
+       Name string `json:"name"`
+}
+
+type DockerHubTagsResponse struct {
+       //Count   int            `json:"count"`
+       Results []DockerHubTag `json:"results"`
+}
+
+// Version cache
+var (
+       lastVersionCheck     time.Time
+       cachedLatestVersion  string
+       cachedLatestWithTest string
+       versionCacheDuration = time.Hour
+)
+
+// parseVersion parses a semver string and returns major, minor, patch as integers
+// Returns -1, -1, -1 if parsing fails
+func parseVersion(version string) (int, int, int) {
+       // Remove 'v' prefix if present
+       version = strings.TrimPrefix(version, "v")
+
+       // Split by '-' to separate version from pre-release identifiers
+       parts := strings.Split(version, "-")
+       if len(parts) == 0 {
+               return -1, -1, -1
+       }
+
+       // Parse the main version part (e.g., "2.3.1")
+       versionPart := parts[0]
+       versionNumbers := strings.Split(versionPart, ".")
+
+       if len(versionNumbers) != 3 {
+               return -1, -1, -1
+       }
+
+       major, err1 := strconv.Atoi(versionNumbers[0])
+       minor, err2 := strconv.Atoi(versionNumbers[1])
+       patch, err3 := strconv.Atoi(versionNumbers[2])
+
+       if err1 != nil || err2 != nil || err3 != nil {
+               return -1, -1, -1
+       }
+
+       return major, minor, patch
+}
+
+// compareVersions compares two version strings
+// Returns 1 if v1 > v2, -1 if v1 < v2, 0 if equal
+func compareVersions(v1, v2 string) int {
+       maj1, min1, pat1 := parseVersion(v1)
+       maj2, min2, pat2 := parseVersion(v2)
+
+       // If either version is invalid, treat it as lower
+       if maj1 == -1 {
+               if maj2 == -1 {
+                       return 0
+               }
+               return -1
+       }
+       if maj2 == -1 {
+               return 1
+       }
+
+       // Compare major version
+       if maj1 != maj2 {
+               if maj1 > maj2 {
+                       return 1
+               }
+               return -1
+       }
+
+       // Compare minor version
+       if min1 != min2 {
+               if min1 > min2 {
+                       return 1
+               }
+               return -1
+       }
+
+       // Compare patch version
+       if pat1 != pat2 {
+               if pat1 > pat2 {
+                       return 1
+               }
+               return -1
+       }
+
+       return 0
+}
+
+// isStableVersion checks if a version is stable (no pre-release identifiers like "testing")
+func isStableVersion(version string) bool {
+       // Remove 'v' prefix if present
+       version = strings.TrimPrefix(version, "v")
+
+       // Convert to lowercase for case-insensitive search
+       lowerVersion := strings.ToLower(version)
+
+       // Check if the version contains "test" (part of "testing", "test", etc.)
+       if strings.Contains(lowerVersion, "test") {
+               return false
+       }
+
+       return true
+}
+
+// GetLatestVersion fetches the latest version information from Docker Hub
+// Returns (latest_stable_version, latest_version_including_testing)
+func GetLatestVersion() (string, string) {
+       // Check if we have cached data that's still fresh
+       if time.Since(lastVersionCheck) < versionCacheDuration && cachedLatestVersion != "" {
+               return cachedLatestVersion, cachedLatestWithTest
+       }
+
+       // Fetch tags from Docker Hub
+       resp, err := http.Get("https://hub.docker.com/v2/repositories/phitux/dailytxt/tags")
+       if err != nil {
+               Logger.Printf("Error fetching Docker Hub tags: %v", err)
+               // Return cached values if available, otherwise empty
+               return cachedLatestVersion, cachedLatestWithTest
+       }
+       defer resp.Body.Close()
+
+       if resp.StatusCode != http.StatusOK {
+               Logger.Printf("Docker Hub API returned status %d", resp.StatusCode)
+               return cachedLatestVersion, cachedLatestWithTest
+       }
+
+       body, err := io.ReadAll(resp.Body)
+       if err != nil {
+               Logger.Printf("Error reading Docker Hub response: %v", err)
+               return cachedLatestVersion, cachedLatestWithTest
+       }
+
+       var tagsResponse DockerHubTagsResponse
+       if err := json.Unmarshal(body, &tagsResponse); err != nil {
+               Logger.Printf("Error parsing Docker Hub response: %v", err)
+               return cachedLatestVersion, cachedLatestWithTest
+       }
+
+       var latestStable, latestOverall string
+
+       // Process all tags
+       for _, tag := range tagsResponse.Results {
+               tagName := tag.Name
+
+               // Skip non-version tags like "latest"
+               if !strings.Contains(tagName, ".") {
+                       continue
+               }
+
+               // Check if this is a valid semver-like version
+               maj, min, pat := parseVersion(tagName)
+               if maj == -1 || min == -1 || pat == -1 {
+                       continue
+               }
+
+               // Update latest overall version
+               if latestOverall == "" || compareVersions(tagName, latestOverall) > 0 {
+                       latestOverall = tagName
+               }
+
+               // Update latest stable version (only if it's stable)
+               if isStableVersion(tagName) {
+                       if latestStable == "" || compareVersions(tagName, latestStable) > 0 {
+                               latestStable = tagName
+                       }
+               }
+       }
+
+       // Update cache
+       lastVersionCheck = time.Now()
+       cachedLatestVersion = latestStable
+       cachedLatestWithTest = latestOverall
+
+       return latestStable, latestOverall
+}
index ad8c698387b9aefa4b6add024c1aa3297a9e3e41..5a5e3875864706b391115630fc383f9c8855c3b2 100644 (file)
@@ -24,7 +24,8 @@
                faCopy,
                faCheck,
                faSun,
-               faMoon
+               faMoon,
+               faCircleUp
        } from '@fortawesome/free-solid-svg-icons';
        import Tag from '$lib/Tag.svelte';
        import SelectTimezone from '$lib/SelectTimezone.svelte';
@@ -34,6 +35,8 @@
        import Statistics from '$lib/settings/Statistics.svelte';
        import Admin from '$lib/settings/Admin.svelte';
        import { T, getTranslate, getTolgee } from '@tolgee/svelte';
+       import github from '$lib/assets/GitHub-Logo.png';
+       import donate from '$lib/assets/bmc-button.png';
 
        const { t } = getTranslate();
        const tolgee = getTolgee(['language']);
        let inDuration = 150;
        let outDuration = 150;
 
+       let current_version = $state('');
+       let latest_stable_version = $state('');
+       let latest_overall_version = $state('');
+       let updateAvailable = $state(false);
+
        // Active sub-view of settings modal: 'settings' | 'stats' | 'admin'
        let activeSettingsView = $state('settings');
 
+       // Function to compare version strings (semver-like)
+       function compareVersions(v1, v2) {
+               if (!v1 || !v2) return 0;
+
+               const parseVersion = (version) => {
+                       const cleaned = version.replace(/^v/, '');
+                       const parts = cleaned.split('-')[0].split('.');
+                       return parts.map((part) => parseInt(part) || 0);
+               };
+
+               const version1 = parseVersion(v1);
+               const version2 = parseVersion(v2);
+
+               for (let i = 0; i < Math.max(version1.length, version2.length); i++) {
+                       const v1Part = version1[i] || 0;
+                       const v2Part = version2[i] || 0;
+
+                       if (v1Part > v2Part) return 1;
+                       if (v1Part < v2Part) return -1;
+               }
+
+               // if both have the same semver-number, check the testing-number (like 2.3.1-testing.3)
+               // if one does not have anything on the right of "-", then this is the "stable" version
+               const testingVersion1 = v1.split('-')[1] || '';
+               const testingVersion2 = v2.split('-')[1] || '';
+
+               if (testingVersion1 === '') return 1;
+               if (testingVersion2 === '') return -1;
+
+               return testingVersion1.localeCompare(testingVersion2) > 0;
+       }
+
+       // Function to check if updates are available
+       function checkForUpdates() {
+               if (!$settings.checkForUpdates) {
+                       updateAvailable = false;
+                       return;
+               }
+
+               const latestVersion = $settings.includeTestVersions
+                       ? latest_overall_version
+                       : latest_stable_version;
+
+               updateAvailable = compareVersions(latestVersion, current_version) > 0;
+       }
+
+       // React to changes in settings or version info
+       $effect(() => {
+               checkForUpdates();
+       });
+
        $effect(() => {
                if ($readingMode === true && page.url.pathname !== '/read') {
                        goto('/read');
        onMount(() => {
                getUserSettings();
                getTemplates();
+               getVersionInfo();
 
                if (page.url.pathname === '/read') {
                        $readingMode = true;
                                isExporting = false;
                        });
        }
+
+       function getVersionInfo() {
+               axios
+                       .get(API_URL + '/version')
+                       .then((response) => {
+                               current_version = response.data.current_version;
+                               latest_stable_version = response.data.latest_stable_version;
+                               latest_overall_version = response.data.latest_overall_version;
+                               // Trigger update check after loading version info
+                               checkForUpdates();
+                       })
+                       .catch((error) => {
+                               console.error('Error fetching version info:', error);
+                       });
+       }
 </script>
 
 <div class="d-flex flex-column h-100">
                        </div>
 
                        <div class="col-lg-4 col-sm-5 col pe-0 d-flex flex-row justify-content-end">
-                               <button class="btn btn-outline-secondary me-2" onclick={openSettingsModal}
-                                       ><Fa icon={faSliders} /></button
+                               <button
+                                       class="btn btn-outline-secondary me-2 position-relative"
+                                       onclick={openSettingsModal}
                                >
+                                       <Fa icon={faSliders} />
+                                       {#if updateAvailable}
+                                               <Fa
+                                                       icon={faCircleUp}
+                                                       size="1.2x"
+                                                       class="position-absolute top-0 start-100 translate-middle text-info"
+                                               />
+                                       {/if}
+                               </button>
                                <button class="btn btn-outline-secondary" onclick={() => logout(null)}
                                        ><Fa icon={faRightFromBracket} /></button
                                >
                                                                                class="nav-link mb-1 text-start {activeSettingsSection === 'about'
                                                                                        ? 'active'
                                                                                        : ''}"
-                                                                               onclick={() => scrollToSection('about')}>{$t('settings.about')}</button
+                                                                               onclick={() => scrollToSection('about')}
                                                                        >
+                                                                               {$t('settings.about')}
+                                                                               {#if updateAvailable}
+                                                                                       <Fa icon={faCircleUp} size="1.2x" class="text-info" />
+                                                                               {/if}
+                                                                       </button>
                                                                </nav>
                                                        </nav>
                                                </div>
 
                                                                <div id="about">
                                                                        <h3 class="text-primary">💡 {$t('settings.about')}</h3>
-                                                                       Version:<br />
-                                                                       Changelog: <br />
-                                                                       Link zu github
+
+                                                                       <span class="d-table mx-auto"
+                                                                               >{@html $t('settings.about.made_by', {
+                                                                                       creator:
+                                                                                               '<a class="link-light link-underline link-underline-opacity-0 link-underline-opacity-75-hover" href="https://github.com/PhiTux" target="_blank">PhiTux / Marco Kümmel</a>'
+                                                                               })}</span
+                                                                       >
+                                                                       <hr />
+
+                                                                       <u>{$t('settings.about.current_version')}:</u>
+                                                                       <b>{current_version}</b><br />
+                                                                       <u>{$t('settings.about.latest_version')}:</u>
+                                                                       {#if !updateAvailable}
+                                                                               <b
+                                                                                       >{$settings.includeTestVersions
+                                                                                               ? latest_overall_version
+                                                                                               : latest_stable_version}</b
+                                                                               >
+                                                                       {:else}
+                                                                               <a href="https://hub.docker.com/r/phitux/dailytxt/tags" target="_blank"
+                                                                                       ><span class="badge text-bg-info fs-6"
+                                                                                               >{$settings.includeTestVersions
+                                                                                                       ? latest_overall_version
+                                                                                                       : latest_stable_version}</span
+                                                                                       ></a
+                                                                               >
+                                                                       {/if}
+
+                                                                       <br />
+
+                                                                       {#if updateAvailable}
+                                                                               <p class="alert alert-info d-flex align-items-center mt-2 mb-2 p-2">
+                                                                                       <Fa icon={faCircleUp} size="2x" class="text-info me-2" />
+                                                                                       {$t('settings.about.update_available')}
+                                                                               </p>
+                                                                       {/if}
+
+                                                                       <span class="form-text">{$t('settings.about.version_info')}</span><br />
+
+                                                                       <a
+                                                                               class="btn btn-secondary my-2"
+                                                                               href="https://github.com/PhiTux/DailyTxT#changelog"
+                                                                               target="_blank"
+                                                                       >
+                                                                               {$t('settings.about.changelog')}
+                                                                       </a>
+
+                                                                       <div id="updateSettings" class="mt-2">
+                                                                               {#if $tempSettings.checkForUpdates !== $settings.checkForUpdates || $tempSettings.includeTestVersions !== $settings.includeTestVersions}
+                                                                                       {@render unsavedChanges()}
+                                                                               {/if}
+
+                                                                               <h5>{$t('settings.about.update_notification')}</h5>
+                                                                               <div class="form-check form-switch">
+                                                                                       <input
+                                                                                               class="form-check-input"
+                                                                                               bind:checked={$tempSettings.checkForUpdates}
+                                                                                               type="checkbox"
+                                                                                               role="switch"
+                                                                                               id="checkForUpdatesSwitch"
+                                                                                       />
+                                                                                       <label class="form-check-label" for="checkForUpdatesSwitch">
+                                                                                               {$t('settings.updates.check_for_updates')}
+                                                                                       </label>
+                                                                               </div>
+
+                                                                               <div class="form-check form-switch ms-3">
+                                                                                       <input
+                                                                                               class="form-check-input"
+                                                                                               bind:checked={$tempSettings.includeTestVersions}
+                                                                                               type="checkbox"
+                                                                                               role="switch"
+                                                                                               id="includeTestVersionsSwitch"
+                                                                                               disabled={!$tempSettings.checkForUpdates}
+                                                                                       />
+                                                                                       <label class="form-check-label" for="includeTestVersionsSwitch">
+                                                                                               {$t('settings.updates.include_test_versions')}
+                                                                                       </label>
+                                                                               </div>
+                                                                       </div>
+
+                                                                       <hr />
+
+                                                                       <a
+                                                                               class="btn btn-secondary mx-auto d-table"
+                                                                               href="https://github.com/PhiTux/DailyTxT"
+                                                                               target="_blank"
+                                                                       >
+                                                                               {$t('settings.about.source_code')}: <img src={github} alt="" width="100px" />
+                                                                       </a>
+
+                                                                       <hr />
+
+                                                                       <span class="d-table mx-auto">{@html $t('settings.about.donate')}</span>
+                                                                       <a
+                                                                               class="d-block mx-auto mt-2"
+                                                                               href="https://www.buymeacoffee.com/PhiTux"
+                                                                               target="_blank"
+                                                                               style="width: 200px;"
+                                                                       >
+                                                                               <img src={donate} alt="" width="200px" />
+                                                                       </a>
                                                                </div>
                                                        </div>
                                                </div>
git clone https://git.99rst.org/PROJECT