From f6b696a5f81916f3f8fec207126a354850563a20 Mon Sep 17 00:00:00 2001 From: Andrey Kondratev <81143241+cockroach-eater@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:27:58 +0500 Subject: [PATCH] cache versions --- .gitignore | 3 ++ package.json | 5 +- public/index.html | 5 ++ public/script.ts | 103 ++++++++++++++++++++++++++++++++++++ public/style.css | 39 ++++++++++++++ scripts/generate-version.js | 46 ++++++++++++++++ src/server.ts | 46 ++++++++++++++-- 7 files changed, 242 insertions(+), 5 deletions(-) create mode 100644 scripts/generate-version.js diff --git a/.gitignore b/.gitignore index c930e70..3363467 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,9 @@ build/ out/ coverage/ +# Version file (generated at build time) +public/version.json + # Backup files *.bak *.backup diff --git a/package.json b/package.json index b0488fd..1112e2e 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,12 @@ "description": "Telegram miniapp for YouTube music search and MP3 conversion", "main": "dist/server.js", "scripts": { - "build": "tsc && tsc -p tsconfig.frontend.json", + "build": "node scripts/generate-version.js && tsc && tsc -p tsconfig.frontend.json", "build:backend": "tsc", "build:frontend": "tsc -p tsconfig.frontend.json", - "build:prod": "yarn build && node scripts/minify.js", + "build:prod": "node scripts/generate-version.js && yarn build && node scripts/minify.js", "minify": "node scripts/minify.js", + "version": "node scripts/generate-version.js", "start": "node dist/server.js", "dev": "ts-node src/server.ts", "dev:watch": "nodemon --exec ts-node src/server.ts", diff --git a/public/index.html b/public/index.html index daf96e7..98c8abe 100644 --- a/public/index.html +++ b/public/index.html @@ -12,6 +12,11 @@ + + + + + diff --git a/public/script.ts b/public/script.ts index 2eba831..243562a 100644 --- a/public/script.ts +++ b/public/script.ts @@ -62,12 +62,115 @@ class QuixoticApp { private hasMoreResults: boolean = false; private isLoadingMore: boolean = false; private scrollObserver: IntersectionObserver | null = null; + private currentVersion: string | null = null; constructor() { this.tg = (window as WindowWithTelegram).Telegram?.WebApp; this.loadRecentSearches(); this.init(); this.bindEvents(); + this.checkVersion(); // Check for updates on load + } + + private async checkVersion(): Promise { + try { + // Get current version from localStorage + const storedVersion = localStorage.getItem('appVersion'); + + // Fetch latest version from server + const response = await fetch('/api/version', { + cache: 'no-cache', + headers: { + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache' + } + }); + + if (!response.ok) return; + + const versionData = await response.json(); + const serverVersion = versionData.version; + + this.currentVersion = serverVersion; + + // If versions don't match, force reload + if (storedVersion && storedVersion !== serverVersion) { + console.log('🔄 New version detected, updating...'); + + // Clear cache and reload + if ('caches' in window) { + const cacheNames = await caches.keys(); + await Promise.all(cacheNames.map(name => caches.delete(name))); + } + + // Store new version + localStorage.setItem('appVersion', serverVersion); + + // Force hard reload + window.location.reload(); + return; + } + + // Store version for future checks + if (!storedVersion) { + localStorage.setItem('appVersion', serverVersion); + } + + // Periodically check for updates (every 5 minutes) + setInterval(() => this.silentVersionCheck(), 5 * 60 * 1000); + + } catch (error) { + console.warn('Version check failed:', error); + } + } + + private async silentVersionCheck(): Promise { + try { + const response = await fetch('/api/version', { + cache: 'no-cache', + headers: { + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache' + } + }); + + if (!response.ok) return; + + const versionData = await response.json(); + const serverVersion = versionData.version; + + if (this.currentVersion && this.currentVersion !== serverVersion) { + console.log('🔄 Update available'); + + // Show update notification + this.showUpdateNotification(); + } + } catch (error) { + console.warn('Silent version check failed:', error); + } + } + + private showUpdateNotification(): void { + const notification = document.createElement('div'); + notification.className = 'tg-update-notification'; + notification.innerHTML = ` +
+ Доступно обновление + +
+ `; + + const button = notification.querySelector('button'); + button?.addEventListener('click', () => { + window.location.reload(); + }); + + document.body.appendChild(notification); + + // Auto-dismiss after 30 seconds + setTimeout(() => { + notification.remove(); + }, 30000); } private triggerHaptic(type: 'light' | 'medium' | 'heavy' | 'success' | 'error' = 'light'): void { diff --git a/public/style.css b/public/style.css index 71b8dc2..61fd299 100644 --- a/public/style.css +++ b/public/style.css @@ -696,6 +696,45 @@ body { } } +/* Update notification */ +.tg-update-notification { + position: fixed; + top: var(--tg-spacing-lg); + left: var(--tg-spacing-lg); + right: var(--tg-spacing-lg); + z-index: 1000; + animation: tg-slide-down 0.3s ease-out; +} + +.tg-update-notification__content { + background: var(--tg-color-button); + color: var(--tg-color-button-text); + padding: var(--tg-spacing-md) var(--tg-spacing-lg); + border-radius: var(--tg-border-radius); + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + font-weight: var(--tg-font-weight-medium); +} + +.tg-update-notification__button { + background: rgba(255, 255, 255, 0.2); + color: var(--tg-color-button-text); + border: none; + padding: var(--tg-spacing-sm) var(--tg-spacing-lg); + border-radius: var(--tg-border-radius-small); + font-size: var(--tg-font-size-sm); + font-weight: var(--tg-font-weight-semibold); + cursor: pointer; + transition: background 0.2s ease; + -webkit-tap-highlight-color: transparent; +} + +.tg-update-notification__button:active { + background: rgba(255, 255, 255, 0.3); +} + /* Recent Searches */ .tg-recent-searches { margin-bottom: var(--tg-spacing-lg); diff --git a/scripts/generate-version.js b/scripts/generate-version.js new file mode 100644 index 0000000..f3781aa --- /dev/null +++ b/scripts/generate-version.js @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +function generateVersion() { + const timestamp = Date.now(); + const date = new Date().toISOString(); + + let gitHash = null; + let gitBranch = null; + + try { + gitHash = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim(); + gitBranch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim(); + } catch (e) { + console.warn('Warning: Could not get git info'); + } + + const version = { + timestamp, + date, + version: gitHash ? `${timestamp}-${gitHash}` : timestamp.toString(), + gitHash, + gitBranch, + buildDate: date + }; + + const outputPath = path.join(__dirname, '../public/version.json'); + fs.writeFileSync(outputPath, JSON.stringify(version, null, 2)); + + console.log('✅ Version file generated:', version.version); + console.log(` Date: ${date}`); + if (gitHash) { + console.log(` Git: ${gitHash} (${gitBranch})`); + } + + return version; +} + +if (require.main === module) { + generateVersion(); +} + +module.exports = { generateVersion }; diff --git a/src/server.ts b/src/server.ts index 2f98af4..cb5dfa7 100644 --- a/src/server.ts +++ b/src/server.ts @@ -69,6 +69,19 @@ if (!fs.existsSync(downloadsDir)) { fs.mkdirSync(downloadsDir, { recursive: true }); } +// Load version for cache busting +let appVersion = Date.now().toString(); +try { + const versionPath = path.join(__dirname, '../public/version.json'); + if (fs.existsSync(versionPath)) { + const versionData = JSON.parse(fs.readFileSync(versionPath, 'utf8')); + appVersion = versionData.version || appVersion; + logger.info(`App version loaded: ${appVersion}`); + } +} catch (error) { + logger.warn('Could not load version file, using timestamp'); +} + // Routes app.get('/', (req: Request, res: Response) => { // Use minified HTML in production @@ -76,12 +89,23 @@ app.get('/', (req: Request, res: Response) => { const htmlFile = isProduction ? 'index.min.html' : 'index.html'; const indexPath = path.join(__dirname, '../public', htmlFile); - // Set cache headers for HTML (short cache) + // Set cache headers for HTML (no cache for HTML itself) res.set({ - 'Cache-Control': 'public, max-age=3600, must-revalidate', // 1 hour, revalidate + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' }); - res.sendFile(indexPath); + // Read HTML and inject version + try { + let html = fs.readFileSync(indexPath, 'utf8'); + // Replace version placeholders with actual version + html = html.replace(/\?v=\d+/g, `?v=${appVersion}`); + res.send(html); + } catch (error) { + logger.error('Error serving HTML:', error); + res.sendFile(indexPath); + } }); // Search videos @@ -288,6 +312,22 @@ app.get('/health', (req: Request, res: Response) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); +// Version endpoint for client-side cache busting +app.get('/api/version', (req: Request, res: Response) => { + res.set('Cache-Control', 'no-cache, no-store, must-revalidate'); + try { + const versionPath = path.join(__dirname, '../public/version.json'); + if (fs.existsSync(versionPath)) { + const versionData = fs.readFileSync(versionPath, 'utf8'); + res.json(JSON.parse(versionData)); + } else { + res.json({ version: appVersion, timestamp: Date.now() }); + } + } catch (error) { + res.json({ version: appVersion, timestamp: Date.now() }); + } +}); + // Error handler app.use((err: Error, _req: Request, res: Response, _next: any) => { logger.error(err.stack || err.message);