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,
+ })
+}
"useDarkMode": false,
"background": "gradient",
"monochromeBackgroundColor": "#ececec",
+ "checkForUpdates": true,
+ "includeTestVersions": false,
}
}
"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,
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)
// 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)
"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)
// 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
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
+}
faCopy,
faCheck,
faSun,
- faMoon
+ faMoon,
+ faCircleUp
} from '@fortawesome/free-solid-svg-icons';
import Tag from '$lib/Tag.svelte';
import SelectTimezone from '$lib/SelectTimezone.svelte';
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>