{
+ 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);