This commit is contained in:
Andrey Kondratev
2025-11-07 21:12:49 +05:00
parent 5d7c6b2a09
commit ca27a2b3f0
3 changed files with 62 additions and 11 deletions

50
docker-compose.local.yml Normal file
View File

@@ -0,0 +1,50 @@
services:
postgres:
image: postgres:15-alpine
container_name: quixotic-postgres
restart: unless-stopped
environment:
POSTGRES_DB: quixotic
POSTGRES_USER: quixotic
POSTGRES_PASSWORD: quixotic123
volumes:
- postgres-data:/var/lib/postgresql/data
- ./database/init:/docker-entrypoint-initdb.d
networks:
- quixotic
healthcheck:
test: ["CMD-SHELL", "pg_isready -U quixotic"]
interval: 5s
timeout: 5s
retries: 5
quixotic-app:
build:
context: .
dockerfile: Dockerfile
container_name: quixotic-app
restart: unless-stopped
environment:
NODE_ENV: production
PORT: 3000
DATABASE_URL: postgresql://quixotic:quixotic123@postgres:5432/quixotic
DATABASE_SSL: false
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
WEB_APP_URL: http://localhost:3000
volumes:
- downloads:/app/downloads
ports:
- "3000:3000"
depends_on:
postgres:
condition: service_healthy
networks:
- quixotic
volumes:
downloads:
postgres-data:
networks:
quixotic:
driver: bridge

View File

@@ -60,11 +60,12 @@
<!-- Critical CSS - inline the most important styles --> <!-- Critical CSS - inline the most important styles -->
<style> <style>
:root{--tg-color-bg:var(--tg-theme-bg-color,#ffffff);--tg-color-secondary-bg:var(--tg-theme-secondary-bg-color,#f1f1f1);--tg-color-section-bg:var(--tg-theme-section-bg-color,#ffffff);--tg-color-text:var(--tg-theme-text-color,#000000);--tg-color-hint:var(--tg-theme-hint-color,#999999);--tg-color-link:var(--tg-theme-link-color,#007aff);--tg-color-button:var(--tg-theme-button-color,#007aff);--tg-color-button-text:var(--tg-theme-button-text-color,#ffffff);--tg-border-radius:12px;--tg-spacing-lg:16px;--tg-spacing-xl:20px;--tg-spacing-xxl:24px;--tg-font-size-md:16px;--tg-font-size-lg:17px;--tg-font-size-xl:20px;--tg-line-height-normal:1.4;--tg-line-height-relaxed:1.6}*{margin:0;padding:0;box-sizing:border-box}html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Display','SF Pro Text',system-ui,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{background:var(--tg-color-bg);color:var(--tg-color-text);font-size:var(--tg-font-size-md);line-height:var(--tg-line-height-normal);overflow-x:hidden}.tg-root{min-height:100vh;display:flex;flex-direction:column}.tg-content{flex:1;padding:var(--tg-spacing-lg);padding-bottom:100px;display:flex;flex-direction:column;gap:var(--tg-spacing-xl)}.tg-placeholder{text-align:center;padding:var(--tg-spacing-xxl) var(--tg-spacing-lg);max-width:300px;margin:0 auto}.tg-placeholder__icon{font-size:48px;margin-bottom:var(--tg-spacing-lg);opacity:.6}.tg-placeholder__title{font-size:var(--tg-font-size-xl);font-weight:600;color:var(--tg-color-text);margin-bottom:8px}.tg-placeholder__description{font-size:14px;color:var(--tg-color-hint);line-height:var(--tg-line-height-relaxed)}.tg-hidden{display:none!important} :root{--tg-color-bg:var(--tg-theme-bg-color,#fff);--tg-color-secondary-bg:var(--tg-theme-secondary-bg-color,#f1f1f1);--tg-color-section-bg:var(--tg-theme-section-bg-color,#fff);--tg-color-text:var(--tg-theme-text-color,#000);--tg-color-hint:var(--tg-theme-hint-color,#999);--tg-color-button:var(--tg-theme-button-color,#007aff);--tg-color-button-text:var(--tg-theme-button-text-color,#fff);--tg-border-radius:12px;--tg-spacing-lg:16px;--tg-spacing-xl:20px;--tg-spacing-xxl:24px;--tg-font-size-md:16px;--tg-font-size-lg:17px;--tg-font-size-xl:20px;--tg-line-height-normal:1.4;--tg-line-height-relaxed:1.6}*{margin:0;padding:0;box-sizing:border-box}html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Display',system-ui,sans-serif;-webkit-font-smoothing:antialiased}body{background:var(--tg-color-bg);color:var(--tg-color-text);font-size:var(--tg-font-size-md);line-height:var(--tg-line-height-normal);overflow-x:hidden}.tg-root{min-height:100vh;display:flex;flex-direction:column}.tg-content{flex:1;padding:var(--tg-spacing-lg);padding-bottom:100px;display:flex;flex-direction:column;gap:var(--tg-spacing-xl)}.tg-placeholder{text-align:center;padding:var(--tg-spacing-xxl) var(--tg-spacing-lg);max-width:300px;margin:0 auto}.tg-placeholder__icon{font-size:48px;margin-bottom:var(--tg-spacing-lg);opacity:.6}.tg-placeholder__title{font-size:var(--tg-font-size-xl);font-weight:600;margin-bottom:8px}.tg-placeholder__description{font-size:14px;color:var(--tg-color-hint);line-height:var(--tg-line-height-relaxed)}.tg-hidden{display:none!important}.tg-form{position:fixed;bottom:0;left:0;right:0;padding:var(--tg-spacing-lg);background:var(--tg-color-bg);border-top:1px solid var(--tg-color-secondary-bg);z-index:100}.tg-input-wrapper{position:relative}.tg-input{width:100%;height:48px;padding:0 var(--tg-spacing-lg);background:var(--tg-color-section-bg);border:2px solid var(--tg-color-secondary-bg);border-radius:var(--tg-border-radius);font-size:var(--tg-font-size-lg);color:var(--tg-color-text);transition:border-color .2s;outline:0}.tg-input::placeholder{color:var(--tg-color-hint)}.tg-input:focus{border-color:var(--tg-color-button)}
</style> </style>
<!-- Load full CSS asynchronously --> <!-- Load full CSS asynchronously with fallback -->
<link rel="stylesheet" href="style.css" media="print" onload="this.media='all'; this.onload=null;"> <link rel="preload" href="style.css?v=3" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="style.css?v=3"></noscript>
<!-- Load Telegram script asynchronously (defer) --> <!-- Load Telegram script asynchronously (defer) -->
<script src="https://telegram.org/js/telegram-web-app.js" defer></script> <script src="https://telegram.org/js/telegram-web-app.js" defer></script>
@@ -106,6 +107,6 @@
</div> </div>
<!-- Load app script with defer for better performance --> <!-- Load app script with defer for better performance -->
<script src="dist/script.js?v=2" defer></script> <script src="dist/script.js?v=3" defer></script>
</body> </body>
</html> </html>

View File

@@ -43,21 +43,21 @@ app.use((req: Request, res: Response, next) => {
// Optimized caching strategy // Optimized caching strategy
app.use(express.static('public', { app.use(express.static('public', {
maxAge: '1d', // Cache static assets for 1 day maxAge: '365d', // Cache static assets for 1 year by default
etag: true, etag: true,
lastModified: true, lastModified: true,
setHeaders: (res: Response, filePath: string) => { setHeaders: (res: Response, filePath: string) => {
// Cache images, fonts, etc. longer // Cache images, fonts, etc. with immutable flag
if (filePath.match(/\.(jpg|jpeg|png|gif|ico|woff|woff2|ttf|eot)$/)) { if (filePath.match(/\.(jpg|jpeg|png|gif|ico|woff|woff2|ttf|eot|svg)$/)) {
res.set('Cache-Control', 'public, max-age=31536000, immutable'); res.set('Cache-Control', 'public, max-age=31536000, immutable');
} }
// Cache CSS and JS with version string // Cache CSS and JS with version string for 1 year
else if (filePath.match(/\.(css|js)$/)) { else if (filePath.match(/\.(css|js)$/)) {
res.set('Cache-Control', 'public, max-age=86400'); // 1 day res.set('Cache-Control', 'public, max-age=31536000, immutable'); // 1 year
} }
// HTML files - short cache // HTML files - short cache with revalidation
else if (filePath.match(/\.html$/)) { else if (filePath.match(/\.html$/)) {
res.set('Cache-Control', 'public, max-age=3600'); // 1 hour res.set('Cache-Control', 'public, max-age=0, must-revalidate');
} }
} }
})); }));