This commit is contained in:
Andrey Kondratev
2025-08-29 10:57:50 +05:00
parent 6bde4bfd4c
commit b483ed71f2
11 changed files with 106 additions and 50 deletions

View File

@@ -34,13 +34,7 @@ jobs:
- name: Run yarn audit - name: Run yarn audit
run: yarn audit --level high run: yarn audit --level high
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: javascript
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
docker-security: docker-security:
name: Docker Security Scan name: Docker Security Scan
@@ -59,4 +53,3 @@ jobs:
image-ref: 'quixotic:scan' image-ref: 'quixotic:scan'
format: 'sarif' format: 'sarif'
output: 'trivy-results.sarif' output: 'trivy-results.sarif'

View File

@@ -0,0 +1,31 @@
# Layout Issue Analysis
## Problem Description
User reports that the application layout is broken:
1. "Nothing found" notification shows always
2. Search field that was at bottom is now at top
## Current Structure Analysis
Based on code review of public/index.html and public/script.ts:
### HTML Structure (Current):
1. Search form (input + button) - at TOP
2. Welcome placeholder - initially visible
3. Loading spinner - hidden by default
4. Results list - hidden by default
5. No results placeholder - hidden by default
### JavaScript Logic Issues Found:
1. **No Results always showing**: In `showNoResults()` method, the noResults element is made visible with `classList.remove('tg-hidden')` but there's no logic to hide it when new search starts or results are found.
2. **Search field position**: The search form is structurally at the top in HTML, which matches user complaint.
### Display Logic Flow:
- `showLoading()`: Hides welcome, shows loading, hides results, hides noResults
- `displayResults()`: Calls `hideLoading()`, shows results if any, otherwise calls `showNoResults()`
- `showNoResults()`: Calls `hideLoading()`, hides results, SHOWS noResults (but doesn't hide welcome)
## Root Causes:
1. **NoResults visibility issue**: The noResults element is never hidden when starting a new search
2. **Welcome placeholder management**: Not properly hidden when showing noResults
3. **Search position**: Structurally at top, needs to be moved to bottom if that was the intended design

View File

@@ -0,0 +1,18 @@
# Telegram Bot Token Fix
## Problem
Docker container was showing "⚠️ TELEGRAM_BOT_TOKEN not found or invalid - bot will not start" error despite the token being present in `.env.docker` file.
## Root Cause
The `docker-compose.yml` file was missing the `env_file` configuration to load environment variables from `.env.docker`.
## Solution
Added `env_file: - .env.docker` at the top level of docker-compose.yml to ensure all services load environment variables from the .env.docker file.
## Files Modified
- `docker-compose.yml`: Added env_file configuration
## Token Details
- Token is present in `.env.docker`: `8262100335:AAFUBycadhKwV4oWPtF_Uq3c_R95SfWUElM`
- WEB_APP_URL is configured: `https://quixy.uk`
- Environment variables are properly referenced in services

View File

@@ -4,6 +4,8 @@ services:
image: traefik:v3.0 image: traefik:v3.0
container_name: quixotic-traefik container_name: quixotic-traefik
restart: unless-stopped restart: unless-stopped
env_file:
- .env.docker
command: command:
- --api.dashboard=true - --api.dashboard=true
- --api.insecure=false - --api.insecure=false
@@ -37,6 +39,8 @@ services:
image: postgres:15-alpine image: postgres:15-alpine
container_name: quixotic-postgres container_name: quixotic-postgres
restart: unless-stopped restart: unless-stopped
env_file:
- .env.docker
environment: environment:
POSTGRES_DB: ${POSTGRES_DB:-quixotic} POSTGRES_DB: ${POSTGRES_DB:-quixotic}
POSTGRES_USER: ${POSTGRES_USER:-quixotic} POSTGRES_USER: ${POSTGRES_USER:-quixotic}
@@ -59,10 +63,13 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: quixotic-app container_name: quixotic-app
restart: unless-stopped restart: unless-stopped
env_file:
- .env.docker
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- PORT=3000 - PORT=3000
- DATABASE_URL=postgresql://${POSTGRES_USER:-quixotic}:${POSTGRES_PASSWORD:-quixotic123}@postgres:5432/${POSTGRES_DB:-quixotic} - DATABASE_URL=postgresql://${POSTGRES_USER:-quixotic}:${POSTGRES_PASSWORD:-quixotic123}@postgres:5432/${POSTGRES_DB:-quixotic}
- DATABASE_SSL=false
volumes: volumes:
- downloads:/app/downloads - downloads:/app/downloads
labels: labels:

View File

@@ -12,34 +12,28 @@
</head> </head>
<body> <body>
<div class="tg-root"> <div class="tg-root">
</div>
<div class="tg-content"> <div class="tg-content">
<div class="tg-placeholder" id="welcomePlaceholder"> <div class="tg-placeholder" id="welcomePlaceholder">
<div class="tg-placeholder__icon">🎵</div> <div class="tg-placeholder__icon">🎵</div>
<div class="tg-placeholder__title">Найти музыку</div> <div class="tg-placeholder__title">Найти музыку</div>
<div class="tg-placeholder__description">Введите название песни или исполнителя для поиска на YouTube</div> <div class="tg-placeholder__description">Введите название песни или исполнителя для поиска на SoundCloud</div>
</div> </div>
<button class="tg-button tg-button--primary tg-button--large" id="searchBtn"> <div class="tg-spinner tg-hidden" id="loading">
<span class="tg-button__text">Найти</span>
</button>
</div>
<div class="tg-spinner" id="loading">
<div class="tg-spinner__icon"></div> <div class="tg-spinner__icon"></div>
<div class="tg-spinner__text">Поиск музыки...</div> <div class="tg-spinner__text">Поиск музыки...</div>
</div> </div>
<div class="tg-list" id="results"> <div class="tg-list tg-hidden" id="results">
<!-- Search results will appear here --> <!-- Search results will appear here -->
</div> </div>
<div class="tg-placeholder tg-placeholder--secondary" id="noResults"> <div class="tg-placeholder tg-placeholder--secondary tg-hidden" id="noResults">
<div class="tg-placeholder__icon">🔍</div> <div class="tg-placeholder__icon">🔍</div>
<div class="tg-placeholder__title">Ничего не найдено</div> <div class="tg-placeholder__title">Ничего не найдено</div>
<div class="tg-placeholder__description">Попробуйте изменить поисковый запрос</div> <div class="tg-placeholder__description">Попробуйте изменить поисковый запрос</div>
</div> </div>
<div class="tg-form"> <div class="tg-form">
<div class="tg-input-wrapper"> <div class="tg-input-wrapper">
<input type="text" <input type="text"

View File

@@ -110,6 +110,7 @@ class QuixoticApp {
this.welcomePlaceholder.classList.add('tg-hidden'); this.welcomePlaceholder.classList.add('tg-hidden');
this.loading.classList.remove('tg-hidden'); this.loading.classList.remove('tg-hidden');
this.loading.classList.add('tg-spinner--visible'); this.loading.classList.add('tg-spinner--visible');
this.results.classList.add('tg-hidden');
this.results.classList.remove('tg-list--visible'); this.results.classList.remove('tg-list--visible');
this.noResults.classList.add('tg-hidden'); this.noResults.classList.add('tg-hidden');
this.searchBtn.disabled = true; this.searchBtn.disabled = true;
@@ -147,12 +148,14 @@ class QuixoticApp {
</div> </div>
`).join(''); `).join('');
this.results.classList.remove('tg-hidden');
this.results.classList.add('tg-list--visible'); this.results.classList.add('tg-list--visible');
this.noResults.classList.add('tg-hidden'); this.noResults.classList.add('tg-hidden');
} }
private showNoResults(): void { private showNoResults(): void {
this.hideLoading(); this.hideLoading();
this.results.classList.add('tg-hidden');
this.results.classList.remove('tg-list--visible'); this.results.classList.remove('tg-list--visible');
this.noResults.classList.remove('tg-hidden'); this.noResults.classList.remove('tg-hidden');
} }

View File

@@ -67,6 +67,7 @@ body {
.tg-content { .tg-content {
flex: 1; flex: 1;
padding: var(--tg-spacing-lg); padding: var(--tg-spacing-lg);
padding-bottom: 140px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--tg-spacing-xl); gap: var(--tg-spacing-xl);
@@ -74,9 +75,17 @@ body {
/* Form components */ /* Form components */
.tg-form { .tg-form {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--tg-spacing-md); gap: var(--tg-spacing-md);
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 { .tg-input-wrapper {

View File

@@ -84,7 +84,7 @@ export class QuixoticBot {
await this.bot.sendMessage(chatId, await this.bot.sendMessage(chatId,
'🎵 Добро пожаловать в Quixotic!\n\n' + '🎵 Добро пожаловать в Quixotic!\n\n' +
'Найди любую песню на YouTube и получи MP3 файл прямо в чат.\n\n' + 'Найди любую песню на SoundCloud и получи MP3 файл прямо в чат.\n\n' +
'Нажми кнопку ниже, чтобы начать поиск:', 'Нажми кнопку ниже, чтобы начать поиск:',
{ reply_markup: keyboard } { reply_markup: keyboard }
); );
@@ -98,7 +98,7 @@ export class QuixoticBot {
this.bot.onText(/\/help/, async (msg: Message) => { this.bot.onText(/\/help/, async (msg: Message) => {
const chatId = msg.chat.id; const chatId = msg.chat.id;
const helpText = `🎵 *Quixotic - YouTube to MP3* const helpText = `🎵 *Quixotic - SoundCloud to MP3*
*Как пользоваться:* *Как пользоваться:*
1⃣ Нажми кнопку "Открыть Quixotic" 1⃣ Нажми кнопку "Открыть Quixotic"
@@ -112,8 +112,8 @@ export class QuixoticBot {
/history - История поиска /history - История поиска
*Возможности:* *Возможности:*
✅ Поиск по YouTube ✅ Поиск по SoundCloud
✅ Высокое качество MP3 (128kbps) ✅ Высокое качество MP3 (192kbps)
✅ Быстрая конвертация ✅ Быстрая конвертация
✅ История поиска`; ✅ История поиска`;
@@ -218,19 +218,7 @@ export class QuixoticBot {
} }
private async getSearchHistory(userId: number): Promise<SearchResult[]> { private async getSearchHistory(userId: number): Promise<SearchResult[]> {
return new Promise((resolve, reject) => { return this.db.getSearchHistory(userId);
this.db['db'].all(
`SELECT query, created_at FROM search_history
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT 10`,
[userId],
(err: Error | null, rows: SearchResult[]) => {
if (err) reject(err);
else resolve(rows || []);
}
);
});
} }
private async sendAudioFile(chatId: number, audioUrl: string, title: string): Promise<void> { private async sendAudioFile(chatId: number, audioUrl: string, title: string): Promise<void> {

View File

@@ -1,4 +1,4 @@
import { Pool, QueryResult } from 'pg'; import { Pool } from 'pg';
interface TelegramUser { interface TelegramUser {
id: number; id: number;
@@ -52,7 +52,7 @@ export class Database {
await this.pool.query(`CREATE TABLE IF NOT EXISTS downloads ( await this.pool.query(`CREATE TABLE IF NOT EXISTS downloads (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id), user_id INTEGER REFERENCES users(id),
youtube_id TEXT NOT NULL, track_id TEXT NOT NULL,
title TEXT NOT NULL, title TEXT NOT NULL,
file_path TEXT, file_path TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
@@ -85,10 +85,10 @@ export class Database {
return result.rows[0].id; return result.rows[0].id;
} }
async addDownload(userId: number, youtubeId: string, title: string, filePath: string): Promise<number> { async addDownload(userId: number, trackId: string, title: string, filePath: string): Promise<number> {
const result = await this.pool.query( const result = await this.pool.query(
'INSERT INTO downloads (user_id, youtube_id, title, file_path) VALUES ($1, $2, $3, $4) RETURNING id', 'INSERT INTO downloads (user_id, track_id, title, file_path) VALUES ($1, $2, $3, $4) RETURNING id',
[userId, youtubeId, title, filePath] [userId, trackId, title, filePath]
); );
return result.rows[0].id; return result.rows[0].id;
} }
@@ -101,6 +101,14 @@ export class Database {
return result.rows[0] || undefined; return result.rows[0] || undefined;
} }
async getSearchHistory(userId: number, limit: number = 10): Promise<{query: string, created_at: string}[]> {
const result = await this.pool.query(
'SELECT query, created_at FROM search_history WHERE user_id = $1 ORDER BY created_at DESC LIMIT $2',
[userId, limit]
);
return result.rows;
}
async close(): Promise<void> { async close(): Promise<void> {
await this.pool.end(); await this.pool.end();
} }

View File

@@ -198,8 +198,8 @@ app.get('/health', (req: Request, res: Response) => {
}); });
// Error handler // Error handler
app.use((err: Error, req: Request, res: Response, next: any) => { app.use((_err: Error, _req: Request, res: Response, _next: any) => {
console.error(err.stack); console.error(_err.stack);
res.status(500).json({ error: 'Something went wrong!' }); res.status(500).json({ error: 'Something went wrong!' });
}); });
@@ -238,11 +238,16 @@ app.listen(port, () => {
const botToken = process.env.TELEGRAM_BOT_TOKEN; const botToken = process.env.TELEGRAM_BOT_TOKEN;
const webAppUrl = process.env.WEB_APP_URL || `http://localhost:${port}`; const webAppUrl = process.env.WEB_APP_URL || `http://localhost:${port}`;
if (botToken) { if (botToken && botToken.length > 10) {
const bot = new QuixoticBot(botToken, webAppUrl); try {
console.log('🤖 Telegram bot started'); new QuixoticBot(botToken, webAppUrl);
console.log('🤖 Telegram bot started');
} catch (error: any) {
console.error('❌ Bot initialization failed:', error.message);
console.warn('⚠️ Bot disabled due to error');
}
} else { } else {
console.warn('⚠️ TELEGRAM_BOT_TOKEN not found - bot will not start'); console.warn('⚠️ TELEGRAM_BOT_TOKEN not found or invalid - bot will not start');
} }
export default app; export default app;