Kgshop commited on
Commit
484ce69
·
verified ·
1 Parent(s): 914ebcc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +356 -307
app.py CHANGED
@@ -1,61 +1,54 @@
1
  import os
2
  import uuid
 
 
 
3
  import json
4
  import logging
5
  import threading
6
- import time
7
  from datetime import datetime
8
- from flask import Flask, request, jsonify, Response, send_from_directory, flash
 
 
 
9
  import google.generativeai as genai
10
  from huggingface_hub import HfApi, hf_hub_download
11
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
12
- import requests
13
  from dotenv import load_dotenv
 
14
 
15
- # --- 1. КОНФИГУРАЦИЯ И ИНИЦИАЛИЗАЦИЯ ---
16
-
17
- # Загружаем переменные окружения из файла .env
18
  load_dotenv()
19
-
20
  app = Flask(__name__)
21
- # Секретный ключ нужен для flash-сообщений (хотя мы их здесь не используем, это хорошая практика)
22
- app.secret_key = os.getenv("FLASK_SECRET_KEY", "a_very_secret_key_for_eva_generator")
23
-
24
- # --- Конфигурация Hugging Face (из Кода 2) ---
25
- # Укажите ваш репозиторий на Hugging Face
26
- REPO_ID = "Kgshop/testsynk"
27
- HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") # Токен с правами на запись
28
- HF_TOKEN_READ = os.getenv("HF_TOKEN_READ", HF_TOKEN_WRITE) # Токен для чтения (можно использовать токен на запись)
29
 
30
- # --- Конфигурация Google AI ---
31
- # Ключ API теперь берется из переменных окружения
32
- GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
33
-
34
- # --- Настройки файловой системы ---
35
- # Директория для хранения сгенерированных Python-сайтов
36
  GENERATED_SITES_DIR = 'generated_sites'
37
- # Имя файла для нашей локальной "базы данных" с метаданными о сайтах
38
- DATA_FILE = 'generated_sites_db.json'
39
- # Список файлов для синхронизации с Hugging Face
 
 
 
40
  SYNC_FILES = [DATA_FILE]
41
 
42
- # Настройка логирования для отладки
 
 
 
 
43
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
44
 
45
- # Создаем директорию для сайтов, если ее нет
46
  if not os.path.exists(GENERATED_SITES_DIR):
47
  os.makedirs(GENERATED_SITES_DIR)
48
 
49
 
50
- # --- 2. СИСТЕМА СИНХРОНИЗАЦИИ С HUGGING FACE (из Кода 2) ---
51
-
52
- DOWNLOAD_RETRIES = 3
53
- DOWNLOAD_DELAY = 5
54
 
55
- def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
56
  if not HF_TOKEN_READ:
57
- logging.warning("HF_TOKEN_READ/WRITE not set. Download might fail for private repos.")
58
-
59
  files_to_download = [specific_file] if specific_file else SYNC_FILES
60
  logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...")
61
  all_successful = True
@@ -64,11 +57,10 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
64
  success = False
65
  for attempt in range(retries + 1):
66
  try:
67
- logging.info(f"Downloading {file_name} (Attempt {attempt + 1}/{retries + 1})...")
68
  hf_hub_download(
69
- repo_id=REPO_ID, filename=file_name, repo_type="dataset",
70
- token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False,
71
- force_download=True, resume_download=False
72
  )
73
  logging.info(f"Successfully downloaded {file_name}.")
74
  success = True
@@ -77,120 +69,186 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
77
  if e.response.status_code == 404:
78
  logging.warning(f"File {file_name} not found in repo {REPO_ID}. Creating empty local file.")
79
  if not os.path.exists(file_name):
80
- try:
81
- if file_name == DATA_FILE:
82
- with open(file_name, 'w', encoding='utf-8') as f:
83
- json.dump({'sites': {}}, f) # Структура данных для этого проекта
84
- logging.info(f"Created empty local file {file_name}.")
85
- except Exception as create_e:
86
- logging.error(f"Failed to create empty local file {file_name}: {create_e}")
87
- success = True # Считаем успехом, т.к. создали пустой файл
88
  break
89
- else:
90
- logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying...")
91
  except Exception as e:
92
- logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying...", exc_info=True)
93
-
94
  if attempt < retries: time.sleep(delay)
95
-
96
  if not success:
97
- logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
98
  all_successful = False
99
-
100
- logging.info(f"Download process finished. Overall success: {all_successful}")
101
  return all_successful
102
 
103
  def upload_db_to_hf(specific_file=None):
104
- if not HF_TOKEN_WRITE:
105
- logging.warning("HF_TOKEN_WRITE not set. Skipping upload to Hugging Face.")
106
  return
107
  try:
108
  api = HfApi()
109
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
110
  logging.info(f"Starting upload of {files_to_upload} to HF repo {REPO_ID}...")
111
-
112
  for file_name in files_to_upload:
113
  if os.path.exists(file_name):
114
  api.upload_file(
115
- path_or_fileobj=file_name, path_in_repo=file_name,
116
- repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
117
  commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
118
  )
119
- logging.info(f"File {file_name} successfully uploaded to Hugging Face.")
120
  else:
121
  logging.warning(f"File {file_name} not found locally, skipping upload.")
122
  except Exception as e:
123
  logging.error(f"Error during Hugging Face upload: {e}", exc_info=True)
124
 
125
  def periodic_backup():
126
- backup_interval = 1800 # 30 минут
127
- logging.info(f"Setting up periodic backup every {backup_interval} seconds.")
128
  while True:
129
- time.sleep(backup_interval)
130
  logging.info("Starting periodic backup...")
131
  upload_db_to_hf()
132
- logging.info("Periodic backup finished.")
133
 
134
  def load_data():
135
- default_data = {'sites': {}}
136
  if not os.path.exists(DATA_FILE):
137
- logging.warning(f"{DATA_FILE} not found locally. Attempting to download from HF.")
138
- download_db_from_hf(specific_file=DATA_FILE)
139
-
140
  try:
141
- with open(DATA_FILE, 'r', encoding='utf-8') as file:
142
- data = json.load(file)
143
- if not isinstance(data, dict) or 'sites' not in data:
144
- logging.error(f"Data in {DATA_FILE} is corrupted. Using default.")
145
- return default_data
146
  return data
147
  except (FileNotFoundError, json.JSONDecodeError):
148
- logging.error(f"Could not read or parse {DATA_FILE}. Using default and creating a new one.")
149
- with open(DATA_FILE, 'w', encoding='utf-8') as f:
150
- json.dump(default_data, f)
151
- return default_data
152
 
153
  def save_data(data):
154
  try:
155
- with open(DATA_FILE, 'w', encoding='utf-8') as file:
156
- json.dump(data, file, ensure_ascii=False, indent=4)
157
- logging.info(f"Data successfully saved to {DATA_FILE}")
158
- # Запускаем загрузку в HF в отдельном потоке, чтобы не блокировать ответ пользователю
159
- threading.Thread(target=upload_db_to_hf, args=(DATA_FILE,)).start()
160
  except Exception as e:
161
- logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
 
162
 
 
163
 
164
- # --- 3. ЛОГИКА ГЕНЕРАЦИИ КОДА ---
 
 
 
 
 
 
165
 
166
- def generate_python_flask_app(user_prompt):
167
- """
168
- Генерирует код полноценного однофайлового Flask-приложения по запросу пользователя.
169
- """
170
- if not GOOGLE_API_KEY:
171
- raise ValueError("GOOGLE_API_KEY не настроен в переменных окружения.")
172
 
173
  try:
174
- genai.configure(api_key=GOOGLE_API_KEY)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  except Exception as e:
176
- raise ValueError(f"Не удалось настроить Google AI. Проверьте GOOGLE_API_KEY. Ошибка: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
  if not user_prompt or not user_prompt.strip():
179
- raise ValueError("Текстовый запрос (промпт) не может быть пустым.")
180
 
181
- # КЛЮЧЕВОЕ ИЗМЕНЕНИЕ: Системный промпт для генерации Python кода
182
  system_instruction = (
183
- "You are an expert Python Flask developer. Your task is to generate a complete, single-file Python Flask application "
184
- "based on the user's request. The entire application must be contained within one single `.py` file.\n"
185
- "Follow these rules strictly:\n"
186
- "1. **Self-Contained:** The script must be fully self-contained. All HTML, CSS, and JavaScript must be embedded within Python multi-line string variables (templates). Do not attempt to read external template files.\n"
187
- "2. **Imports:** Include all necessary imports at the top of the script (e.g., `os`, `json`, `uuid`, `from flask import Flask, request, jsonify, render_template_string, redirect, url_for`).\n"
188
- "3. **Database:** For data persistence, use a simple JSON file as a database. The script should handle creating, reading, and writing to this JSON file (e.g., `db.json`). The database logic should be part of the generated script.\n"
189
- "4. **Structure:** The script should define the Flask app, necessary HTML/CSS/JS templates as strings, Flask routes (`@app.route(...)`), and data handling functions. Ensure the app is well-commented and easy to understand.\n"
190
- "5. **Execution:** The script must include the standard `if __name__ == '__main__':` block to run the Flask development server, making it runnable with a single command `python <filename>.py`.\n"
191
- "6. **Functionality:** The generated app should be fully functional according to the user's request, visually clean, and user-friendly. Use modern, responsive CSS.\n"
192
- "7. **Output Format:** Directly output ONLY the raw Python code. Do NOT wrap it in markdown blocks like ```python ... ```. Do not include any explanatory text, titles, or comments before `import` or after the `app.run()` call. The response must start with `import` and end with `)`. "
193
- "8. **Example App Type:** For a request 'a simple blog', generate a Flask app with routes for viewing all posts, viewing a single post, and a form to add a new post. It should save posts in a `blog_posts.json` file."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  )
195
 
196
  full_prompt = f"{system_instruction}\n\nUser request: \"{user_prompt}\""
@@ -198,257 +256,248 @@ def generate_python_flask_app(user_prompt):
198
  try:
199
  model = genai.GenerativeModel('gemini-1.5-flash-latest') # Используем современную модель
200
  response = model.generate_content(full_prompt)
 
201
 
202
- # Обработка ответа от новой модели Gemini
203
- if not response.parts:
204
- if hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
205
- reason = response.prompt_feedback.block_reason
206
- raise ValueError(f"Генерация контента заблокирована (причина: {reason}). Попробуйте другой запрос.")
207
- else:
208
- raise ValueError("Модель вернула пустой результат. Попробуйте изменить запрос.")
209
-
210
- generated_text = "".join(part.text for part in response.parts)
211
 
212
- # Очистка и проверка результата
213
- clean_text = generated_text.strip()
214
- if clean_text.lower().startswith("```python"):
215
- clean_text = clean_text[9:]
216
- if clean_text.endswith("```"):
217
- clean_text = clean_text[:-3]
218
-
219
- if not clean_text.strip().lower().startswith("import"):
220
- print(f"Warning: Output might not be pure Python. Preview: {clean_text[:200]}")
221
- # Можно добавить дополнительную логику очистки, если понадобится
222
 
223
- return clean_text.strip()
224
 
225
  except Exception as e:
226
- logging.error(f"Error generating content with GenAI: {e}")
227
- error_message = str(e)
228
- if "API key not valid" in error_message:
229
- raise ValueError("Внутренняя ошибка: API ключ Google недействителен.")
230
- elif "quota" in error_message.lower():
231
- raise ValueError("Квота запросов к Google AI исчерпана. Попробуйте позже.")
232
- else:
233
- raise ValueError(f"Ошибка при генерации Python-кода: {e}")
234
 
 
235
 
236
- # --- 4. FRONTEND И ОСНОВНЫЕ ROUTES ---
237
-
238
- # HTML-шаблон для главной страницы. Обновлен текст и логика JS
239
- html_template = """
240
  <!DOCTYPE html>
241
  <html lang="ru">
242
  <head>
243
  <meta charset="UTF-8">
244
- <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
245
- <title>EVA - Генератор Python Flask Сайтов</title>
246
  <style>
247
- :root {
248
- --system-gray-100-light: #f2f2f7; --system-gray-75-light: #f8f8fa; --system-gray-50-light: #ffffff; --system-gray-dark-100-light: #000000; --system-gray-dark-75-light: #1c1c1e; --system-gray-dark-50-light: #3a3a3c; --system-gray-light-75-light: #8e8e93; --system-gray-light-50-light: #aeaeb2; --system-blue-light: #007aff; --system-blue-light-hover: #005ecf; --system-red-light: #ff3b30; --system-separator-light: rgba(60, 60, 67, 0.29); --system-separator-opaque-light: #d1d1d6;
249
- --system-gray-100-dark: #1c1c1e; --system-gray-75-dark: #2c2c2e; --system-gray-50-dark: #000000; --system-gray-dark-100-dark: #ffffff; --system-gray-dark-75-dark: #f2f2f7; --system-gray-dark-50-dark: #e5e5ea; --system-gray-light-75-dark: #8e8e93; --system-gray-light-50-dark: #636366; --system-blue-dark: #0a84ff; --system-blue-dark-hover: #3b9eff; --system-red-dark: #ff453a; --system-separator-dark: rgba(84, 84, 88, 0.65); --system-separator-opaque-dark: #38383a;
250
- }
251
- @media (prefers-color-scheme: dark) { :root { --bg-color: var(--system-gray-50-dark); --content-bg: var(--system-gray-100-dark); --text-color: var(--system-gray-dark-100-dark); --secondary-text-color: var(--system-gray-light-75-dark); --tertiary-text-color: var(--system-gray-light-50-dark); --border-color: var(--system-separator-dark); --border-color-opaque: var(--system-separator-opaque-dark); --input-bg: var(--system-gray-75-dark); --primary-color: var(--system-blue-dark); --primary-color-hover: var(--system-blue-dark-hover); --error-color: var(--system-red-dark); } }
252
- @media (prefers-color-scheme: light) { :root { --bg-color: var(--system-gray-100-light); --content-bg: var(--system-gray-50-light); --text-color: var(--system-gray-dark-100-light); --secondary-text-color: var(--system-gray-light-75-light); --tertiary-text-color: var(--system-gray-light-50-light); --border-color: var(--system-separator-light); --border-color-opaque: var(--system-separator-opaque-light); --input-bg: var(--system-gray-75-light); --primary-color: var(--system-blue-light); --primary-color-hover: var(--system-blue-light-hover); --error-color: var(--system-red-light); } }
253
- html { height: -webkit-fill-available; }
254
- body { font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 20px env(safe-area-inset-top) 20px env(safe-area-inset-bottom); background-color: var(--bg-color); color: var(--text-color); display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; min-height: -webkit-fill-available; line-height: 1.45; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
255
- .container { background-color: var(--content-bg); padding: 25px 30px 30px 30px; border-radius: 24px; box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0,0,0,0.05); max-width: 580px; width: calc(100% - 40px); box-sizing: border-box; margin-top: 30px; }
256
- h1 { font-size: 32px; font-weight: 700; text-align: center; margin-bottom: 8px; color: var(--text-color); letter-spacing: -0.5px; }
257
- p.subtitle { font-size: 17px; color: var(--secondary-text-color); text-align: center; margin-bottom: 35px; font-weight: 400; }
258
- .form-group { margin-bottom: 28px; }
259
- label.input-label { display: block; font-weight: 500; margin-bottom: 10px; font-size: 15px; color: var(--secondary-text-color); padding-left: 5px; }
260
- textarea#prompt-input { width: 100%; padding: 14px 18px; border: 1px solid var(--border-color-opaque); border-radius: 12px; font-size: 16px; background-color: var(--input-bg); color: var(--text-color); box-sizing: border-box; transition: border-color 0.2s ease, box-shadow 0.2s ease; font-family: inherit; resize: vertical; min-height: 120px; }
261
- textarea#prompt-input:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 25%, transparent); outline: none; }
262
- button#generate-button { width: 100%; padding: 16px; background-color: var(--primary-color); color: white; border: none; border-radius: 12px; font-size: 17px; font-weight: 600; cursor: pointer; transition: background-color 0.2s ease, transform 0.1s ease; margin-top: 15px; }
263
- button#generate-button:hover { background-color: var(--primary-color-hover); } button#generate-button:active { transform: scale(0.98); } button#generate-button:disabled { background-color: var(--tertiary-text-color); cursor: not-allowed; }
264
- .output-section { margin-top: 35px; } .output-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } label#output-label { font-weight: 500; font-size: 15px; color: var(--secondary-text-color); padding-left: 5px; }
265
- button#copy-button { background-color: transparent; border: none; color: var(--primary-color); font-size: 14px; font-weight: 500; cursor: pointer; padding: 5px 8px; border-radius: 6px; transition: background-color 0.2s ease, color 0.2s ease; display: none; }
266
- button#copy-button:hover { background-color: color-mix(in srgb, var(--primary-color) 15%, transparent); } button#copy-button:active { background-color: color-mix(in srgb, var(--primary-color) 25%, transparent); } button#copy-button.copied { color: #34c759; } @media (prefers-color-scheme: dark) { button#copy-button.copied { color: #30d158; } }
267
- #output-container { background-color: var(--input-bg); padding: 18px 20px; border-radius: 12px; min-height: 60px; border: 1px solid var(--border-color); word-wrap: break-word; font-size: 15px; color: var(--text-color); line-height: 1.5; transition: border-color 0.2s ease, background-color 0.2s ease; display: flex; align-items: center; justify-content: center; }
268
- #output-container a { color: var(--primary-color); text-decoration: none; font-weight: 500; } #output-container a:hover { text-decoration: underline; }
269
- #output-container.loading::before { content: "Генерация Python-кода..."; display: block; text-align: center; font-style: italic; color: var(--secondary-text-color); animation: fadePulse 1.8s infinite ease-in-out; }
270
- #output-container.error { color: var(--error-color); font-weight: 500; border-color: color-mix(in srgb, var(--error-color) 50%, transparent); background-color: color-mix(in srgb, var(--error-color) 10%, var(--input-bg)); justify-content: flex-start; }
271
- @keyframes fadePulse { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } }
272
- /* Mobile styles omitted for brevity, they are the same as in original file */
273
  </style>
274
  </head>
275
  <body>
276
  <div class="container">
277
- <h1>EVA</h1>
278
- <p class="subtitle">Генератор Python Flask сайтов</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
 
280
- <form id="generate-form">
281
- <div class="form-group">
282
- <label for="prompt-input" class="input-label">Опишите сайт, который вы хотите создать</label>
283
- <textarea id="prompt-input" name="prompt" rows="6" required placeholder="Например: создай сайт-блог с возможностью добавлять посты (заголовок и текст) и удалять их. Посты должны сохраняться в JSON-файл. Главная страница должна отображать все посты."></textarea>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  </div>
285
- <button type="submit" id="generate-button">Создать сайт</button>
286
- </form>
287
-
288
- <div class="output-section">
289
- <div class="output-header">
290
- <label id="output-label">Ссылка на скачивание</label>
291
- <button id="copy-button">Копировать</button>
292
- </div>
293
- <div id="output-container" aria-live="polite"></div>
294
  </div>
295
  </div>
296
-
297
- <script>
298
- const form = document.getElementById('generate-form');
299
- const promptInput = document.getElementById('prompt-input');
300
- const outputContainer = document.getElementById('output-container');
301
- const generateButton = document.getElementById('generate-button');
302
- const copyButton = document.getElementById('copy-button');
303
-
304
- form.addEventListener('submit', async (event) => {
305
- event.preventDefault();
306
- if (!promptInput.value.trim()) { showError("Пожалуйста, опишите сайт."); return; }
307
-
308
- generateButton.disabled = true;
309
- generateButton.textContent = 'Генерация...';
310
- outputContainer.innerHTML = '';
311
- outputContainer.classList.add('loading');
312
- outputContainer.classList.remove('error');
313
- copyButton.style.display = 'none';
314
-
315
- try {
316
- const response = await fetch('/generate', {
317
- method: 'POST',
318
- body: new FormData(form)
319
- });
320
- const result = await response.json();
321
-
322
- if (!response.ok) {
323
- throw new Error(result.error || `Ошибка сервера: ${response.status}`);
324
- }
325
-
326
- // ИЗМЕНЕНО: Обрабатываем ссылку на скачивание
327
- if (result.download_url) {
328
- const link = document.createElement('a');
329
- link.href = result.download_url;
330
- link.textContent = "Скачать сгенерированный сайт (.py)";
331
- link.setAttribute('download', ''); // Атрибут для скачивания
332
-
333
- outputContainer.innerHTML = '';
334
- outputContainer.appendChild(link);
335
-
336
- copyButton.style.display = 'block';
337
- copyButton.dataset.copyText = window.location.origin + result.download_url;
338
- } else {
339
- showError(result.error || "Не удалось получить ссылку на скачивание.");
340
- }
341
-
342
- } catch (error) {
343
- console.error("Fetch Error:", error);
344
- showError(`Ошибка: ${error.message}`);
345
- } finally {
346
- generateButton.disabled = false;
347
- generateButton.textContent = 'Создать сайт';
348
- outputContainer.classList.remove('loading');
349
- }
350
- });
351
-
352
- copyButton.addEventListener('click', () => {
353
- const textToCopy = copyButton.dataset.copyText;
354
- if (!textToCopy) return;
355
- navigator.clipboard.writeText(textToCopy).then(() => {
356
- copyButton.textContent = 'Скопировано!';
357
- copyButton.classList.add('copied');
358
- setTimeout(() => { copyButton.textContent = 'Копировать'; copyButton.classList.remove('copied'); }, 1500);
359
- }).catch(err => { console.error('Ошибка копирования: ', err); });
360
- });
361
-
362
- function showError(message) {
363
- outputContainer.innerHTML = `<span>${message}</span>`;
364
- outputContainer.classList.add('error');
365
- outputContainer.classList.remove('loading');
366
- copyButton.style.display = 'none';
367
- }
368
- </script>
369
  </body>
370
  </html>
371
  """
372
 
373
  @app.route('/')
374
  def index():
375
- return Response(html_template, mimetype='text/html')
376
-
377
- @app.route('/download/<path:filename>')
378
- def download_generated_site(filename):
379
- """
380
- Новый route для скачивания сгенерированных .py файлов.
381
- """
382
- logging.info(f"Request to download file: {filename}")
383
- return send_from_directory(
384
- GENERATED_SITES_DIR,
385
- filename,
386
- as_attachment=True # Этот параметр заставляет браузер скачать файл
387
- )
388
 
389
  @app.route('/generate', methods=['POST'])
390
  def handle_generate():
391
- if 'prompt' not in request.form:
392
- return jsonify({"error": "Текстовый запрос (промпт) не найден."}), 400
393
-
394
- user_prompt = request.form['prompt']
395
- if not user_prompt or not user_prompt.strip():
396
- return jsonify({"error": "Текстовый запрос не может быть пустым."}), 400
397
 
398
  try:
399
- # 1. Генерируем код Python приложения
400
- python_code = generate_python_flask_app(user_prompt)
401
 
402
- if not python_code or not python_code.strip():
403
- return jsonify({"error": "Сгенерированный код пуст. Попробуйте другой запрос."}), 500
404
-
405
- # 2. Сохраняем код в .py файл
406
- site_id = str(uuid.uuid4())
407
  filename = f"{site_id}.py"
408
  filepath = os.path.join(GENERATED_SITES_DIR, filename)
409
 
410
  with open(filepath, "w", encoding="utf-8") as f:
411
  f.write(python_code)
412
 
413
- # 3. Обновляем нашу базу данных
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  data = load_data()
415
- data['sites'][site_id] = {
416
- 'id': site_id,
417
- 'prompt': user_prompt,
418
- 'filename': filename,
419
- 'created_at': datetime.now().isoformat()
420
- }
421
- save_data(data) # Сохраняем локально и асинхронно отправляем в HF
422
-
423
- # 4. Возвращаем ссылку на скачивание
424
- download_url = f"/download/{filename}"
425
- return jsonify({"download_url": download_url})
426
-
427
- except ValueError as ve:
428
- # Ошибки валидации или от API
429
- return jsonify({"error": str(ve)}), 400
430
- except Exception as e:
431
- logging.error(f"Unexpected error during site generation: {e}", exc_info=True)
432
- return jsonify({"error": f"Внутренняя ошибка сервера: {e}"}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
 
434
 
435
- # --- 5. ЗАПУСК ПРИЛОЖЕНИЯ ---
436
 
437
  if __name__ == '__main__':
438
  logging.info("Application starting up...")
439
-
440
- # Первоначальная синхронизация при старте
441
- logging.info("Performing initial data sync from Hugging Face...")
442
  download_db_from_hf()
443
 
444
- # Запуск периодического резервного копирования в фоновом потоке
445
- if HF_TOKEN_WRITE:
 
 
 
 
 
 
 
446
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
447
  backup_thread.start()
448
  logging.info("Periodic backup thread started.")
449
  else:
450
- logging.warning("Periodic backup thread will NOT run (HF_TOKEN_WRITE not set).")
451
 
452
- port = int(os.environ.get('PORT', 7860))
453
- logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
454
- app.run(host='0.0.0.0', port=port, debug=False)
 
1
  import os
2
  import uuid
3
+ import sys
4
+ import subprocess
5
+ import time
6
  import json
7
  import logging
8
  import threading
 
9
  from datetime import datetime
10
+ import signal
11
+ import atexit
12
+
13
+ from flask import Flask, request, jsonify, render_template_string, redirect, url_for, flash
14
  import google.generativeai as genai
15
  from huggingface_hub import HfApi, hf_hub_download
16
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
 
17
  from dotenv import load_dotenv
18
+ import requests
19
 
20
+ # --- 1. Конфигурация и Инициализация ---
 
 
21
  load_dotenv()
 
22
  app = Flask(__name__)
23
+ app.secret_key = 'super_secret_key_for_flask_site_generator_12345'
 
 
 
 
 
 
 
24
 
25
+ # Конфигурация для генератора сайтов
26
+ API_KEY_INTERNAL = "AIzaSyArKidc4o0MwbaCFKStlb2q2AwNg6Pnqns" # Ваш ключ для Google AI
 
 
 
 
27
  GENERATED_SITES_DIR = 'generated_sites'
28
+
29
+ # Конфигурация для Hugging Face Sync
30
+ REPO_ID = "Kgshop/testsynk" # !!! ИЗМЕНИТЕ НА ВАШ РЕПОЗИТОРИЙ !!!
31
+ HF_TOKEN = os.getenv("HF_TOKEN")
32
+ HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN
33
+ DATA_FILE = 'sites_db.json'
34
  SYNC_FILES = [DATA_FILE]
35
 
36
+ # Управление процессами
37
+ running_sites = {} # { "site_id": subprocess_object }
38
+ BASE_PORT = 7861
39
+
40
+ # Настройка логирования
41
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
42
 
 
43
  if not os.path.exists(GENERATED_SITES_DIR):
44
  os.makedirs(GENERATED_SITES_DIR)
45
 
46
 
47
+ # --- 2. Функции для работы с Hugging Face (из Кода 2) ---
 
 
 
48
 
49
+ def download_db_from_hf(specific_file=None, retries=3, delay=5):
50
  if not HF_TOKEN_READ:
51
+ logging.warning("HF_TOKEN_READ not set. Download might fail for private repos.")
 
52
  files_to_download = [specific_file] if specific_file else SYNC_FILES
53
  logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...")
54
  all_successful = True
 
57
  success = False
58
  for attempt in range(retries + 1):
59
  try:
60
+ logging.info(f"Downloading {file_name} (Attempt {attempt + 1})...")
61
  hf_hub_download(
62
+ repo_id=REPO_ID, filename=file_name, repo_type="dataset", token=HF_TOKEN_READ,
63
+ local_dir=".", local_dir_use_symlinks=False, force_download=True, resume_download=False
 
64
  )
65
  logging.info(f"Successfully downloaded {file_name}.")
66
  success = True
 
69
  if e.response.status_code == 404:
70
  logging.warning(f"File {file_name} not found in repo {REPO_ID}. Creating empty local file.")
71
  if not os.path.exists(file_name):
72
+ if file_name == DATA_FILE:
73
+ with open(file_name, 'w', encoding='utf-8') as f:
74
+ json.dump({'sites': {}}, f)
75
+ success = True # Считаем успехом, так как создали пустой файл
 
 
 
 
76
  break
77
+ logging.error(f"HTTP error downloading {file_name}: {e}. Retrying...")
 
78
  except Exception as e:
79
+ logging.error(f"Unexpected error downloading {file_name}: {e}. Retrying...", exc_info=True)
 
80
  if attempt < retries: time.sleep(delay)
 
81
  if not success:
 
82
  all_successful = False
83
+ logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
 
84
  return all_successful
85
 
86
  def upload_db_to_hf(specific_file=None):
87
+ if not HF_TOKEN:
88
+ logging.warning("HF_TOKEN (for writing) not set. Skipping upload.")
89
  return
90
  try:
91
  api = HfApi()
92
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
93
  logging.info(f"Starting upload of {files_to_upload} to HF repo {REPO_ID}...")
 
94
  for file_name in files_to_upload:
95
  if os.path.exists(file_name):
96
  api.upload_file(
97
+ path_or_fileobj=file_name, path_in_repo=file_name, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN,
 
98
  commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
99
  )
100
+ logging.info(f"File {file_name} successfully uploaded.")
101
  else:
102
  logging.warning(f"File {file_name} not found locally, skipping upload.")
103
  except Exception as e:
104
  logging.error(f"Error during Hugging Face upload: {e}", exc_info=True)
105
 
106
  def periodic_backup():
 
 
107
  while True:
108
+ time.sleep(1800) # 30 минут
109
  logging.info("Starting periodic backup...")
110
  upload_db_to_hf()
 
111
 
112
  def load_data():
 
113
  if not os.path.exists(DATA_FILE):
114
+ logging.warning(f"{DATA_FILE} not found locally. Attempting to download.")
115
+ download_db_from_hf(DATA_FILE)
 
116
  try:
117
+ with open(DATA_FILE, 'r', encoding='utf-8') as f:
118
+ data = json.load(f)
119
+ if 'sites' not in data: data['sites'] = {}
 
 
120
  return data
121
  except (FileNotFoundError, json.JSONDecodeError):
122
+ logging.error(f"Failed to load or parse {DATA_FILE}. Using empty structure.")
123
+ return {'sites': {}}
 
 
124
 
125
  def save_data(data):
126
  try:
127
+ with open(DATA_FILE, 'w', encoding='utf-8') as f:
128
+ json.dump(data, f, ensure_ascii=False, indent=4)
129
+ logging.info(f"Data saved to {DATA_FILE}. Uploading to HF.")
130
+ upload_db_to_hf(specific_file=DATA_FILE)
 
131
  except Exception as e:
132
+ logging.error(f"Error saving data: {e}", exc_info=True)
133
+
134
 
135
+ # --- 3. Функции Управления Сайтами и Процессами ---
136
 
137
+ def find_available_port(start_port):
138
+ data = load_data()
139
+ used_ports = {site['port'] for site in data['sites'].values() if 'port' in site and site.get('status') == 'running'}
140
+ port = start_port
141
+ while port in used_ports:
142
+ port += 1
143
+ return port
144
 
145
+ def start_site_process(site_id, filepath):
146
+ """Запускает сайт в отдельном процессе и возвращает объект процесса и порт."""
147
+ port = find_available_port(BASE_PORT)
148
+ env = os.environ.copy()
149
+ env['PORT'] = str(port)
 
150
 
151
  try:
152
+ process = subprocess.Popen(
153
+ [sys.executable, filepath],
154
+ env=env,
155
+ stdout=subprocess.PIPE, # Можно перенаправить в лог-файлы
156
+ stderr=subprocess.PIPE
157
+ )
158
+ time.sleep(2) # Даем время на запуск
159
+
160
+ # Проверяем, жив ли процесс
161
+ if process.poll() is None:
162
+ running_sites[site_id] = process
163
+ return process, port
164
+ else:
165
+ stdout, stderr = process.communicate()
166
+ logging.error(f"Failed to start site {site_id}. Exit code: {process.returncode}")
167
+ logging.error(f"Stderr: {stderr.decode('utf-8', errors='ignore')}")
168
+ return None, None
169
  except Exception as e:
170
+ logging.error(f"Exception while starting site {site_id}: {e}", exc_info=True)
171
+ return None, None
172
+
173
+ def stop_site_process(site_id):
174
+ """Останавливает процесс сайта."""
175
+ data = load_data()
176
+ site_info = data['sites'].get(site_id)
177
+
178
+ if site_id in running_sites:
179
+ process = running_sites[site_id]
180
+ logging.info(f"Stopping running process for site {site_id} with PID {process.pid}")
181
+ process.terminate()
182
+ try:
183
+ process.wait(timeout=5)
184
+ except subprocess.TimeoutExpired:
185
+ process.kill()
186
+ del running_sites[site_id]
187
+ return True
188
+
189
+ # Если процесс не в памяти, но в БД есть PID (после перезапуска)
190
+ if site_info and 'pid' in site_info and site_info.get('status') == 'running':
191
+ pid = site_info['pid']
192
+ logging.warning(f"Process for site {site_id} not in memory. Attempting to kill by PID {pid}")
193
+ try:
194
+ os.kill(pid, signal.SIGTERM)
195
+ return True
196
+ except ProcessLookupError:
197
+ logging.warning(f"Process with PID {pid} not found.")
198
+ return True # Процесса и так нет
199
+ except Exception as e:
200
+ logging.error(f"Failed to kill process with PID {pid}: {e}")
201
+ return False
202
+ return True
203
+
204
+ def cleanup_processes():
205
+ """Останавливает все дочерние процессы при выходе."""
206
+ logging.info("Shutting down. Terminating all running site processes...")
207
+ for site_id, process in list(running_sites.items()):
208
+ logging.info(f"Terminating site {site_id} (PID: {process.pid})")
209
+ process.terminate()
210
+ logging.info("Cleanup complete.")
211
+
212
+ atexit.register(cleanup_processes)
213
+
214
+ # --- 4. Генерация Кода с Помощью ИИ (доработано) ---
215
+
216
+ def generate_website_code_from_prompt(user_prompt):
217
+ try:
218
+ genai.configure(api_key=API_KEY_INTERNAL)
219
+ except Exception as e:
220
+ raise ValueError(f"Ошибка конфигурации Google AI: {e}")
221
 
222
  if not user_prompt or not user_prompt.strip():
223
+ raise ValueError("Промпт не может быть пустым.")
224
 
 
225
  system_instruction = (
226
+ "You are an expert Python Flask web developer. Your mission is to generate a complete, single-file Python Flask web application based on the user's request.\n"
227
+ "The generated script MUST adhere to the following strict rules:\n"
228
+ "1. **Single File:** The entire application, including Flask routes and HTML templates, must be in one single `.py` file.\n"
229
+ "2. **Flask Framework:** The application must use the Flask framework. It should start with `from flask import Flask, render_template_string, ...`.\n"
230
+ "3. **Inline Templates:** All HTML templates must be embedded directly into the Python script as multiline string variables. For example: `HOME_PAGE_TEMPLATE = '''<!DOCTYPE html>...'''`. Use `render_template_string()` to render them.\n"
231
+ "4. **No External Files:** The script must NOT attempt to read from or write to any local files (like .css, .js, .json). All data should be pre-populated in-memory (e.g., as a list of dictionaries). All CSS and JS must be inside the HTML templates within `<style>` and `<script>` tags.\n"
232
+ "5. **Port from Environment:** The application must get its port number from an environment variable named `PORT`. The code to do this is crucial: `port = int(os.environ.get('PORT', 5000))`.\n"
233
+ "6. **Startup Command:** The final executable line of the script MUST be `if __name__ == '__main__': app.run(host='0.0.0.0', port=port)`. This is non-negotiable.\n"
234
+ "7. **Dependencies:** Assume only Flask is installed (`pip install Flask`). Do not use any other external libraries that are not part of the Python standard library.\n"
235
+ "8. **Output Format:** Your output must be ONLY the raw Python code. Do not wrap it in markdown like ```python or add any explanatory text before or after the code block.\n"
236
+ "\n"
237
+ "Example of a valid structure:\n"
238
+ "```python\n"
239
+ "import os\n"
240
+ "from flask import Flask, render_template_string\n\n"
241
+ "app = Flask(__name__)\n\n"
242
+ "PRODUCTS_DATA = [{'id': 1, 'name': 'Toy Car'}, {'id': 2, 'name': 'Plush Bear'}]\n\n"
243
+ "HOME_TEMPLATE = '''\n"
244
+ "<!DOCTYPE html><html>...<body>{% for product in products %}...{% endfor %}</body></html>'''\n\n"
245
+ "@app.route('/')\n"
246
+ "def home():\n"
247
+ " return render_template_string(HOME_TEMPLATE, products=PRODUCTS_DATA)\n\n"
248
+ "if __name__ == '__main__':\n"
249
+ " port = int(os.environ.get('PORT', 5000))\n"
250
+ " app.run(host='0.0.0.0', port=port)\n"
251
+ "```"
252
  )
253
 
254
  full_prompt = f"{system_instruction}\n\nUser request: \"{user_prompt}\""
 
256
  try:
257
  model = genai.GenerativeModel('gemini-1.5-flash-latest') # Используем современную модель
258
  response = model.generate_content(full_prompt)
259
+ generated_code = response.text
260
 
261
+ # Очистка от markdown, если модель его добавила
262
+ if generated_code.strip().startswith("```python"):
263
+ generated_code = generated_code.strip()[9:]
264
+ if generated_code.strip().endswith("```"):
265
+ generated_code = generated_code.strip()[:-3]
 
 
 
 
266
 
267
+ if not generated_code.strip():
268
+ raise ValueError("Модель вернула пустой результат.")
 
 
 
 
 
 
 
 
269
 
270
+ return generated_code.strip()
271
 
272
  except Exception as e:
273
+ logging.error(f"Error generating content with GenAI: {e}", exc_info=True)
274
+ raise ValueError(f"Ошибка при генерации кода сайта: {e}")
 
 
 
 
 
 
275
 
276
+ # --- 5. Flask Routes ---
277
 
278
+ ADMIN_TEMPLATE = """
 
 
 
279
  <!DOCTYPE html>
280
  <html lang="ru">
281
  <head>
282
  <meta charset="UTF-8">
283
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
284
+ <title>Панель Управления Сайтами</title>
285
  <style>
286
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background-color: #f0f2f5; color: #1c1c1e; margin: 0; padding: 20px; }
287
+ .container { max-width: 900px; margin: 20px auto; background: #fff; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
288
+ .header { padding: 20px 25px; border-bottom: 1px solid #e5e5e5; }
289
+ .header h1 { margin: 0; font-size: 24px; }
290
+ .content { padding: 25px; }
291
+ .form-section { margin-bottom: 30px; padding: 20px; background: #f7f7f7; border-radius: 8px; }
292
+ textarea { width: 100%; padding: 12px; border-radius: 6px; border: 1px solid #ccc; font-size: 16px; min-height: 80px; box-sizing: border-box; }
293
+ button { background-color: #007aff; color: white; border: none; padding: 12px 20px; font-size: 16px; font-weight: 500; border-radius: 8px; cursor: pointer; transition: background-color 0.2s; }
294
+ button:hover { background-color: #005ecf; }
295
+ button:disabled { background-color: #aaa; cursor: not-allowed; }
296
+ .site-list { margin-top: 20px; }
297
+ .site-item { display: grid; grid-template-columns: 1fr auto; gap: 15px; align-items: center; padding: 15px; border: 1px solid #e5e5e5; border-radius: 8px; margin-bottom: 10px; background: #fff; }
298
+ .site-info p { margin: 0 0 8px; }
299
+ .site-info p:last-child { margin-bottom: 0; }
300
+ .site-info .prompt { font-style: italic; color: #666; font-size: 14px; }
301
+ .site-actions { display: flex; gap: 10px; flex-wrap: wrap; }
302
+ .site-actions .btn { padding: 8px 12px; font-size: 14px; text-decoration: none; border-radius: 6px; color: white; display: inline-block; }
303
+ .btn-start { background-color: #34c759; } .btn-start:hover { background-color: #2ea34a; }
304
+ .btn-stop { background-color: #ff3b30; } .btn-stop:hover { background-color: #d9332a; }
305
+ .btn-delete { background-color: #8e8e93; } .btn-delete:hover { background-color: #6c6c70; }
306
+ .status { padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: 500; }
307
+ .status.running { background-color: #d4edda; color: #155724; }
308
+ .status.stopped { background-color: #f8d7da; color: #721c24; }
309
+ .flash { padding: 15px; margin-bottom: 20px; border-radius: 8px; }
310
+ .flash.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
311
+ .flash.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
312
  </style>
313
  </head>
314
  <body>
315
  <div class="container">
316
+ <div class="header"><h1>Панель Управления Сайтами</h1></div>
317
+ <div class="content">
318
+ {% with messages = get_flashed_messages(with_categories=true) %}
319
+ {% if messages %}
320
+ {% for category, message in messages %}
321
+ <div class="flash {{ category }}">{{ message }}</div>
322
+ {% endfor %}
323
+ {% endif %}
324
+ {% endwith %}
325
+
326
+ <div class="form-section">
327
+ <h2>Создать новый сайт</h2>
328
+ <form action="{{ url_for('handle_generate') }}" method="POST">
329
+ <textarea name="prompt" required placeholder="Например: создай сайт-визитку для фотографа Анны. На сайте должны быть разделы 'Главная', 'Портфолио' с 6 примерами работ и 'Контакты' с формой обратной связи."></textarea><br><br>
330
+ <button type="submit">Сгенерировать и запустить</button>
331
+ </form>
332
+ </div>
333
 
334
+ <h2>Управление Сайтами</h2>
335
+ <div class="site-list">
336
+ {% if sites %}
337
+ {% for site_id, site in sites.items()|sort(attribute='1.created_at', reverse=True) %}
338
+ <div class="site-item">
339
+ <div class="site-info">
340
+ <p><strong>ID:</strong> {{ site_id }}</p>
341
+ <p class="prompt"><strong>Промпт:</strong> {{ site.prompt }}</p>
342
+ <p><strong>Статус:</strong> <span class="status {{ 'running' if site.status == 'running' else 'stopped' }}">{{ site.status }}</span>
343
+ {% if site.status == 'running' and site.port %}
344
+ | <strong>Порт:</strong> {{ site.port }} | <a href="http://{{ request.host.split(':')[0] }}:{{ site.port }}" target="_blank">Открыть сайт →</a>
345
+ {% endif %}
346
+ </p>
347
+ </div>
348
+ <div class="site-actions">
349
+ {% if site.status == 'stopped' %}
350
+ <a href="{{ url_for('start_site', site_id=site_id) }}" class="btn btn-start">Запустить</a>
351
+ {% else %}
352
+ <a href="{{ url_for('stop_site', site_id=site_id) }}" class="btn btn-stop">Остановить</a>
353
+ {% endif %}
354
+ <a href="{{ url_for('delete_site', site_id=site_id) }}" class="btn btn-delete" onclick="return confirm('Вы уверены, что хотите удалить этот сайт и все его файлы?');">Удалить</a>
355
+ </div>
356
+ </div>
357
+ {% endfor %}
358
+ {% else %}
359
+ <p>Пока не создано ни одного сайта.</p>
360
+ {% endif %}
361
  </div>
 
 
 
 
 
 
 
 
 
362
  </div>
363
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  </body>
365
  </html>
366
  """
367
 
368
  @app.route('/')
369
  def index():
370
+ data = load_data()
371
+ return render_template_string(ADMIN_TEMPLATE, sites=data.get('sites', {}))
 
 
 
 
 
 
 
 
 
 
 
372
 
373
  @app.route('/generate', methods=['POST'])
374
  def handle_generate():
375
+ user_prompt = request.form.get('prompt', '').strip()
376
+ if not user_prompt:
377
+ flash("Промпт не может быть пустым.", 'error')
378
+ return redirect(url_for('index'))
 
 
379
 
380
  try:
381
+ python_code = generate_website_code_from_prompt(user_prompt)
 
382
 
383
+ site_id = uuid.uuid4().hex[:8]
 
 
 
 
384
  filename = f"{site_id}.py"
385
  filepath = os.path.join(GENERATED_SITES_DIR, filename)
386
 
387
  with open(filepath, "w", encoding="utf-8") as f:
388
  f.write(python_code)
389
 
390
+ process, port = start_site_process(site_id, filepath)
391
+ if process and port:
392
+ data = load_data()
393
+ data['sites'][site_id] = {
394
+ 'prompt': user_prompt,
395
+ 'filename': filename,
396
+ 'status': 'running',
397
+ 'port': port,
398
+ 'pid': process.pid,
399
+ 'created_at': datetime.now().isoformat()
400
+ }
401
+ save_data(data)
402
+ flash(f"Сайт '{site_id}' успешно сгенерирован и запущен на порту {port}.", 'success')
403
+ else:
404
+ os.remove(filepath) # Удаляем неудачный файл
405
+ flash(f"Не удалось запустить сгенерированный сайт '{site_id}'. Проверьте логи.", 'error')
406
+
407
+ except (ValueError, Exception) as e:
408
+ flash(f"Ошибка при генерации сайта: {e}", 'error')
409
+
410
+ return redirect(url_for('index'))
411
+
412
+ @app.route('/start/<site_id>')
413
+ def start_site(site_id):
414
+ data = load_data()
415
+ site_info = data['sites'].get(site_id)
416
+ if not site_info:
417
+ flash(f"Сайт с ID {site_id} не найден.", 'error')
418
+ return redirect(url_for('index'))
419
+
420
+ if site_info['status'] == 'running':
421
+ flash(f"Сайт {site_id} уже запущен.", 'error')
422
+ return redirect(url_for('index'))
423
+
424
+ filepath = os.path.join(GENERATED_SITES_DIR, site_info['filename'])
425
+ if not os.path.exists(filepath):
426
+ flash(f"Файл для сайта {site_id} не найден. Возможно, он был удален.", 'error')
427
+ return redirect(url_for('index'))
428
+
429
+ process, port = start_site_process(site_id, filepath)
430
+ if process and port:
431
+ site_info.update({'status': 'running', 'port': port, 'pid': process.pid})
432
+ data['sites'][site_id] = site_info
433
+ save_data(data)
434
+ flash(f"Сайт {site_id} успешно запущен на порту {port}.", 'success')
435
+ else:
436
+ flash(f"Не удалось запустить сайт {site_id}.", 'error')
437
+
438
+ return redirect(url_for('index'))
439
+
440
+ @app.route('/stop/<site_id>')
441
+ def stop_site(site_id):
442
+ if stop_site_process(site_id):
443
  data = load_data()
444
+ if site_id in data['sites']:
445
+ data['sites'][site_id]['status'] = 'stopped'
446
+ data['sites'][site_id].pop('port', None)
447
+ data['sites'][site_id].pop('pid', None)
448
+ save_data(data)
449
+ flash(f"Сайт {site_id} успешно остановлен.", 'success')
450
+ else:
451
+ flash(f"Сайт {site_id} не найден в базе данных, но процесс (если был) остановлен.", 'error')
452
+ else:
453
+ flash(f"Не удалось остановить сайт {site_id}.", 'error')
454
+
455
+ return redirect(url_for('index'))
456
+
457
+ @app.route('/delete/<site_id>')
458
+ def delete_site(site_id):
459
+ data = load_data()
460
+ site_info = data['sites'].get(site_id)
461
+ if not site_info:
462
+ flash(f"Сайт {site_id} не найден.", 'error')
463
+ return redirect(url_for('index'))
464
+
465
+ # Сначала остановить, если запущен
466
+ if site_info['status'] == 'running':
467
+ stop_site_process(site_id)
468
+
469
+ # Удалить файл
470
+ filepath = os.path.join(GENERATED_SITES_DIR, site_info['filename'])
471
+ if os.path.exists(filepath):
472
+ os.remove(filepath)
473
+
474
+ # Удалить из БД
475
+ del data['sites'][site_id]
476
+ save_data(data)
477
+
478
+ flash(f"Сайт {site_id} и его файлы были успешно удалены.", 'success')
479
+ return redirect(url_for('index'))
480
 
481
 
482
+ # --- 6. Запуск Приложения ---
483
 
484
  if __name__ == '__main__':
485
  logging.info("Application starting up...")
 
 
 
486
  download_db_from_hf()
487
 
488
+ # При старте синхронизируем состояние (все сайты считаются остановленными)
489
+ db_data = load_data()
490
+ for site_id in db_data['sites']:
491
+ db_data['sites'][site_id]['status'] = 'stopped'
492
+ db_data['sites'][site_id].pop('port', None)
493
+ db_data['sites'][site_id].pop('pid', None)
494
+ save_data(db_data)
495
+
496
+ if HF_TOKEN:
497
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
498
  backup_thread.start()
499
  logging.info("Periodic backup thread started.")
500
  else:
501
+ logging.warning("Periodic backup will NOT run (HF_TOKEN for writing not set).")
502
 
503
+ app.run(host='0.0.0.0', port=7860, debug=False)