diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,4 +1,4 @@ -from flask import Flask, render_template_string, request, redirect, url_for, jsonify, session, flash +from flask import Flask, render_template_string, request, redirect, url_for, jsonify import json import os import logging @@ -7,73 +7,68 @@ import time from datetime import datetime from huggingface_hub import HfApi, hf_hub_download from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError -from werkzeug.utils import secure_filename -from dotenv import load_dotenv import requests import uuid -import hmac -import hashlib - -load_dotenv() app = Flask(__name__) -app.secret_key = os.getenv("FLASK_SECRET_KEY", 'tontalent_secret_key_telegram_app_12345') -TONTALENT_DATA_FILE = 'tontalent_data.json' +app.secret_key = 'tontalent_secret_key_telegram_mini_app_unique_12345' +DATA_FILE = 'tontalent_data.json' -SYNC_FILES = [TONTALENT_DATA_FILE] +SYNC_FILES = [DATA_FILE] +REPO_ID = "Kgshop/tontalent2" # Replace with your actual Hugging Face repo if used +HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") # For Hugging Face uploads +HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") # For Hugging Face downloads -REPO_ID = "Kgshop/tontalent2" -HF_TOKEN_WRITE = os.getenv("HF_TOKEN") -HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") -BOT_TOKEN = "7549355625:AAGhdbf6x1JEzpH0mUtuxTF83Soi7MFVNZ8" +TELEGRAM_BOT_TOKEN = "7549355625:AAGhdbf6x1JEzpH0mUtuxTF83Soi7MFVNZ8" # Provided by user DOWNLOAD_RETRIES = 3 DOWNLOAD_DELAY = 5 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') -# --- Hugging Face Sync Functions (largely unchanged, adapted for TONTALENT_DATA_FILE) --- def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY): if not HF_TOKEN_READ and not HF_TOKEN_WRITE: logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.") + return False token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE files_to_download = [specific_file] if specific_file else SYNC_FILES logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...") all_successful = True - for file_name in files_to_download: success = False for attempt in range(retries + 1): try: logging.info(f"Downloading {file_name} (Attempt {attempt + 1}/{retries + 1})...") - local_path = hf_hub_download( - repo_id=REPO_ID, filename=file_name, repo_type="dataset", token=token_to_use, - local_dir=".", local_dir_use_symlinks=False, force_download=True, resume_download=False + hf_hub_download( + repo_id=REPO_ID, filename=file_name, repo_type="dataset", + token=token_to_use, local_dir=".", local_dir_use_symlinks=False, + force_download=True, resume_download=False ) - logging.info(f"Successfully downloaded {file_name} to {local_path}.") + logging.info(f"Successfully downloaded {file_name}.") success = True break except RepositoryNotFoundError: - logging.error(f"Repository {REPO_ID} not found. Download cancelled for all files.") + logging.error(f"Repository {REPO_ID} not found. Download cancelled.") return False except HfHubHTTPError as e: if e.response.status_code == 404: - logging.warning(f"File {file_name} not found in repo {REPO_ID} (404). Skipping this file.") - if attempt == 0 and not os.path.exists(file_name) and file_name == TONTALENT_DATA_FILE: + logging.warning(f"File {file_name} not found in repo {REPO_ID} (404).") + if attempt == 0 and not os.path.exists(file_name): try: - with open(file_name, 'w', encoding='utf-8') as f: - json.dump({'resumes': {}, 'vacancies': {}, 'freelance_offers': {}}, f) - logging.info(f"Created empty local file {file_name} because it was not found on HF.") + if file_name == DATA_FILE: + with open(file_name, 'w', encoding='utf-8') as f: + json.dump({'resumes': [], 'vacancies': [], 'freelance_offers': []}, f) + logging.info(f"Created empty local file {file_name}.") except Exception as create_e: logging.error(f"Failed to create empty local file {file_name}: {create_e}") - success = False # Should be false if file not found unless it's the initial creation - break # Don't retry 404 + success = False + break else: - logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...") + logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying...") except requests.exceptions.RequestException as e: - logging.error(f"Network error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...") + logging.error(f"Network error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying...") except Exception as e: - logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True) + logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying...") if attempt < retries: time.sleep(delay) if not success: logging.error(f"Failed to download {file_name} after {retries + 1} attempts.") @@ -83,7 +78,7 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN def upload_db_to_hf(specific_file=None): if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN (for writing) not set. Skipping upload to Hugging Face.") + logging.warning("HF_TOKEN_WRITE not set. Skipping upload to Hugging Face.") return try: api = HfApi() @@ -93,17 +88,18 @@ def upload_db_to_hf(specific_file=None): if os.path.exists(file_name): try: api.upload_file( - path_or_fileobj=file_name, path_in_repo=file_name, repo_id=REPO_ID, repo_type="dataset", - token=HF_TOKEN_WRITE, commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + path_or_fileobj=file_name, path_in_repo=file_name, repo_id=REPO_ID, + repo_type="dataset", token=HF_TOKEN_WRITE, + commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) - logging.info(f"File {file_name} successfully uploaded to Hugging Face.") + logging.info(f"File {file_name} successfully uploaded.") except Exception as e: - logging.error(f"Error uploading file {file_name} to Hugging Face: {e}") + logging.error(f"Error uploading file {file_name}: {e}") else: logging.warning(f"File {file_name} not found locally, skipping upload.") logging.info("Finished uploading files to HF.") except Exception as e: - logging.error(f"General error during Hugging Face upload initialization or process: {e}", exc_info=True) + logging.error(f"General error during Hugging Face upload: {e}") def periodic_backup(): backup_interval = 1800 @@ -114,1108 +110,839 @@ def periodic_backup(): upload_db_to_hf() logging.info("Periodic backup finished.") -# --- Data Loading and Saving Functions --- def load_data(): - default_data = {'resumes': {}, 'vacancies': {}, 'freelance_offers': {}} + default_data = {'resumes': [], 'vacancies': [], 'freelance_offers': []} try: - with open(TONTALENT_DATA_FILE, 'r', encoding='utf-8') as file: + with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) if not isinstance(data, dict): raise FileNotFoundError for key in default_data: - if key not in data: data[key] = default_data[key] - if not isinstance(data[key], dict): data[key] = default_data[key] # Ensure correct type - logging.info(f"Local data loaded successfully from {TONTALENT_DATA_FILE}") + if key not in data: data[key] = [] + logging.info(f"Local data loaded from {DATA_FILE}") return data - except (FileNotFoundError, json.JSONDecodeError) as e: - logging.warning(f"Error loading local file {TONTALENT_DATA_FILE} ({e}). Attempting download from HF.") - - if download_db_from_hf(specific_file=TONTALENT_DATA_FILE): + except (FileNotFoundError, json.JSONDecodeError): + logging.warning(f"Local {DATA_FILE} not found or corrupt. Attempting download.") + if download_db_from_hf(specific_file=DATA_FILE): try: - with open(TONTALENT_DATA_FILE, 'r', encoding='utf-8') as file: - data = json.load(file) - if not isinstance(data, dict): - logging.error(f"Downloaded {TONTALENT_DATA_FILE} is not a dictionary. Using default.") - return default_data + with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) + if not isinstance(data, dict): return default_data for key in default_data: - if key not in data: data[key] = default_data[key] - if not isinstance(data[key], dict): data[key] = default_data[key] - logging.info(f"Data loaded successfully from {TONTALENT_DATA_FILE} after download.") + if key not in data: data[key] = [] + logging.info(f"Data loaded from {DATA_FILE} after download.") return data - except Exception as ex: - logging.error(f"Error loading downloaded {TONTALENT_DATA_FILE}: {ex}. Using default.", exc_info=True) + except Exception as e: + logging.error(f"Error loading downloaded {DATA_FILE}: {e}. Using default.") return default_data else: - logging.error(f"Failed to download {TONTALENT_DATA_FILE} from HF. Using default data structure.") - if not os.path.exists(TONTALENT_DATA_FILE): + logging.error(f"Failed to download {DATA_FILE}. Using empty default data.") + if not os.path.exists(DATA_FILE): try: - with open(TONTALENT_DATA_FILE, 'w', encoding='utf-8') as f: json.dump(default_data, f) - logging.info(f"Created empty local file {TONTALENT_DATA_FILE} after failed download.") + with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump(default_data, f) + logging.info(f"Created empty local file {DATA_FILE}.") except Exception as create_e: - logging.error(f"Failed to create empty local file {TONTALENT_DATA_FILE}: {create_e}") + logging.error(f"Failed to create empty local file {DATA_FILE}: {create_e}") return default_data def save_data(data): try: if not isinstance(data, dict): - logging.error("Attempted to save invalid data structure (not a dict). Aborting save.") + logging.error("Attempted to save invalid data structure. Aborting save.") return - for key in ['resumes', 'vacancies', 'freelance_offers']: # Ensure keys exist - if key not in data: data[key] = {} - - with open(TONTALENT_DATA_FILE, 'w', encoding='utf-8') as file: + default_keys = ['resumes', 'vacancies', 'freelance_offers'] + for key in default_keys: + if key not in data: data[key] = [] + with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) - logging.info(f"Data successfully saved to {TONTALENT_DATA_FILE}") - upload_db_to_hf(specific_file=TONTALENT_DATA_FILE) - except Exception as e: - logging.error(f"Error saving data to {TONTALENT_DATA_FILE}: {e}", exc_info=True) - -# --- Telegram Authentication --- -def validate_telegram_data(init_data_str): - try: - params = dict(kv.split('=', 1) for kv in init_data_str.split('&')) - hash_received = params.pop('hash', None) - if not hash_received: return None - - data_check_string = "\n".join(sorted([f"{k}={v}" for k, v in params.items()])) - - secret_key = hmac.new("WebAppData".encode(), BOT_TOKEN.encode(), hashlib.sha256).digest() - calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest() - - if calculated_hash == hash_received: - user_data_json = params.get('user') - if user_data_json: - return json.loads(requests.utils.unquote(user_data_json)) # Telegram user data needs unquoting - return {} - return None + logging.info(f"Data saved to {DATA_FILE}") + upload_db_to_hf(specific_file=DATA_FILE) except Exception as e: - logging.error(f"Error validating Telegram data: {e}", exc_info=True) - return None + logging.error(f"Error saving data to {DATA_FILE}: {e}") -# --- Templates --- -# Base template including Telegram WebApp JS and basic styling -BASE_TEMPLATE = """ +APP_HTML_TEMPLATE = """ - + TonTalent - -
-
- {{% if 'user' in session and session['user'] %}} - Logged in as: {{ session['user'].get('first_name', '') }} {{ session['user'].get('last_name', '') }} ({{ session['user'].get('username', 'N/A') }}) - {{% else %}} - Authenticating... - {{% endif %}} +
+
+
TonTalent
+
Welcome!
+
+ +
+ + +
- - {{% with messages = get_flashed_messages(with_categories=true) %}} - {{% if messages %}} -
    - {{% for category, message in messages %}} -
  • {{ message }}
  • - {{% endfor %}} -
- {{% endif %}} - {{% endwith %}} - - {{% block content %}}{{% endblock %}} -
- - - - -""".format( - background_color=None, text_color=None, hint_color=None, link_color=None, - button_color=None, button_text_color=None, secondary_bg_color=None, - header_color=None, accent_text_color=None, section_bg_color=None, - section_header_text_color=None, destructive_text_color=None -) # Placeholders for JS to fill - -INDEX_TEMPLATE = """ -{{% extends "base_template" %}} -{{% block content %}} -
-

TonTalent

- Sync Panel -
- - {{% if 'user' in session and session['user'] %}} - - {{% else %}} -

Please wait for authentication to complete to publish or view your items.

- {{% endif %}} - -
- - - -
- -
-

Latest Resumes

- {{% if resumes %}} - {{% for resume_id, resume in resumes.items()|sort(attribute='1.published_at', reverse=True) %}} -
- {{% if resume.photo_filename %}} - {{ resume.full_name }} - {{% endif %}} -

{{ resume.full_name }} - {{ resume.title }}

-

Published: {{ resume.published_at[:10] }}

-

Skills: {{ resume.skills|join(', ') }}

- View Details -
- {{% endfor %}} - {{% else %}} -

No resumes published yet.

- {{% endif %}} -
- -
-

Latest Vacancies

- {{% if vacancies %}} - {{% for vacancy_id, vacancy in vacancies.items()|sort(attribute='1.published_at', reverse=True) %}} -
- {{% if vacancy.company_logo_filename %}} - {{ vacancy.company_name }} - {{% endif %}} -

{{ vacancy.job_title }} at {{ vacancy.company_name }}

-

Published: {{ vacancy.published_at[:10] }}

-

Location: {{ vacancy.location }}

- View Details -
- {{% endfor %}} - {{% else %}} -

No vacancies published yet.

- {{% endif %}} -
- -
-

Latest Freelance Offers

- {{% if freelance_offers %}} - {{% for offer_id, offer in freelance_offers.items()|sort(attribute='1.published_at', reverse=True) %}} -
-

{{ offer.title }}

-

Published: {{ offer.published_at[:10] }}

-

Budget: {{ offer.budget }}

- View Details -
- {{% endfor %}} - {{% else %}} -

No freelance offers published yet.

- {{% endif %}} -
-{{% endblock %}} - -{{% block extra_js %}} - -{{% endblock %}} -""" -PUBLISH_ITEM_TEMPLATE = """ -{{% extends "base_template" %}} -{{% block content %}} -

{{ "Edit" if item_data else "Publish" }} {{ item_type_display_name }}

-
- {{% if item_type == 'resume' %}} -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - - {{% if item_data and item_data.photo_filename %}} -

Current photo: Current Photo

- {{% endif %}} -
- {{% elif item_type == 'vacancy' %}} -
- - -
-
- - -
-
- - -
-
- - +
+
Loading...
-
- - -
-
- - -
-
- - -
-
- - -
-
- - - {{% if item_data and item_data.company_logo_filename %}} -

Current logo: Current Logo

- {{% endif %}} -
- {{% elif item_type == 'freelance_offer' %}} -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- {{% endif %}} - - -{{% endblock %}} -{{% block extra_js %}} - -{{% endblock %}} -""" -VIEW_ITEM_TEMPLATE = """ -{{% extends "base_template" %}} -{{% block content %}} -
- {{% if item_type == 'resume' %}} - {{% if item.photo_filename %}} - {{ item.full_name }} - {{% endif %}} -

{{ item.full_name }}

-

{{ item.title }}

-

Published: {{ item.published_at[:10] }} by {{ item.user_display_name }}

-

Skills: {{ item.skills|join(', ') }}

-

Experience:

- {{% if item.experience %}} -
    {{% for exp in item.experience %}}
  • {{ exp.role }} at {{ exp.company }} ({{ exp.duration }})
    {{ exp.description }}
  • {{% endfor %}}
- {{% else %}}

N/A

{{% endif %}} -

Education:

- {{% if item.education %}} -
    {{% for edu in item.education %}}
  • {{ edu.degree }}, {{ edu.institution }} ({{ edu.duration }})
  • {{% endfor %}}
- {{% else %}}

N/A

{{% endif %}} -

Contact: {{ item.contact_info }}

- {{% if item.portfolio_links %}}

Portfolio: {{ item.portfolio_links|map('urlize')|join(', ')|safe }}

{{% endif %}} - - {{% elif item_type == 'vacancy' %}} - {{% if item.company_logo_filename %}} - {{ item.company_name }} - {{% endif %}} -

{{ item.job_title }}

-

at {{ item.company_name }}

-

Published: {{ item.published_at[:10] }} by {{ item.user_display_name }}

-

Location: {{ item.location }}

-

Description:
{{ item.description|replace('\\n', '
')|safe }}

- {{% if item.requirements %}}

Requirements: {{ item.requirements|join(', ') }}

{{% endif %}} - {{% if item.salary_range %}}

Salary: {{ item.salary_range }}

{{% endif %}} -

Type: {{ item.employment_type }}

-

Contact for Applications: {{ item.contact_info }}

- - {{% elif item_type == 'freelance_offer' %}} -

{{ item.title }}

-

Published: {{ item.published_at[:10] }} by {{ item.user_display_name }}

-

Description:
{{ item.description|replace('\\n', '
')|safe }}

-

Skills Required: {{ item.skills_required|join(', ') }}

-

Budget: {{ item.budget }}

- {{% if item.deadline %}}

Deadline: {{ item.deadline }}

{{% endif %}} -

Contact: {{ item.contact_info }}

- {{% endif %}} - - {{% if 'user' in session and session['user'] and item.user_id == session['user'].id %}} -
- Edit -
- -
-
- {{% endif %}} +
- Back to Listings -{{% endblock %}} -{{% block extra_js %}} - -{{% endblock %}} -""" - -MY_POSTINGS_TEMPLATE = """ -{{% extends "base_template" %}} -{{% block content %}} -

My Postings

- - {{% if not (my_resumes or my_vacancies or my_freelance_offers) %}} -

You haven't published anything yet.

- Publish Something - {{% endif %}} - {{% if my_resumes %}} -

My Resumes

- {{% for resume_id, resume in my_resumes.items() %}} -
- {{% if resume.photo_filename %}} - {{ resume.full_name }} - {{% endif %}} -

{{ resume.full_name }} - {{ resume.title }}

-

Published: {{ resume.published_at[:10] }}

-
- View - Edit -
- +
- {{% endfor %}} - {{% endif %}} - - {{% if my_vacancies %}} -

My Vacancies

- {{% for vacancy_id, vacancy in my_vacancies.items() %}} -
- {{% if vacancy.company_logo_filename %}} - {{ vacancy.company_name }} - {{% endif %}} -

{{ vacancy.job_title }} at {{ vacancy.company_name }}

-

Published: {{ vacancy.published_at[:10] }}

-
- View - Edit -
- -
+ + - {{% endfor %}} - {{% endif %}} - {{% if my_freelance_offers %}} -

My Freelance Offers

- {{% for offer_id, offer in my_freelance_offers.items() %}} -
-

{{ offer.title }}

-

Published: {{ offer.published_at[:10] }}

-
- View - Edit -
- -
-
-
- {{% endfor %}} - {{% endif %}} + -{{% endblock %}} -""" + const formFieldsConfig = { + resumes: ['title', 'skills', 'experience', 'education', 'contact_info', 'description'], + vacancies: ['company_name', 'title', 'location', 'description', 'requirements', 'salary_range', 'contact_info'], + freelance_offers: ['title', 'description', 'skills', 'salary_range', 'timeline', 'contact_info'] + }; -ADMIN_SYNC_TEMPLATE = """ -{{% extends "base_template" %}} -{{% block content %}} -
-

Admin Sync Panel

-
-
-

Sync with Data Store

-
-
- -
-
- -
-
-

Backup occurs automatically every 30 minutes and after each save. Use these buttons for immediate sync.

-
-
-

Data Overview

-

Total Resumes: {{ data_counts.resumes }}

-

Total Vacancies: {{ data_counts.vacancies }}

-

Total Freelance Offers: {{ data_counts.freelance_offers }}

-
- Back to Main Page -{{% endblock %}} -{{% block extra_js %}} - -{{% endblock %}} -""" + function applyTheme(themeParams) { + const root = document.documentElement; + for (const key in themeParams) { + if (key.startsWith('bg_color')) root.style.setProperty('--tg-theme-bg-color', themeParams[key]); + if (key.startsWith('text_color')) root.style.setProperty('--tg-theme-text-color', themeParams[key]); + if (key.startsWith('hint_color')) root.style.setProperty('--tg-theme-hint-color', themeParams[key]); + if (key.startsWith('link_color')) root.style.setProperty('--tg-theme-link-color', themeParams[key]); + if (key.startsWith('button_color')) root.style.setProperty('--tg-theme-button-color', themeParams[key]); + if (key.startsWith('button_text_color')) root.style.setProperty('--tg-theme-button-text-color', themeParams[key]); + if (key.startsWith('secondary_bg_color')) root.style.setProperty('--tg-theme-secondary-bg-color', themeParams[key]); + } + // Additional derivations if not directly provided by older Telegram versions + root.style.setProperty('--tg-theme-header-bg-color', themeParams.header_bg_color || themeParams.bg_color || '#ffffff'); + root.style.setProperty('--tg-theme-section-bg-color', themeParams.section_bg_color || themeParams.bg_color || '#ffffff'); + root.style.setProperty('--tg-theme-destructive-text-color', themeParams.destructive_text_color || '#ff3b30'); + root.style.setProperty('--tg-theme-section-header-text-color', themeParams.section_header_text_color || themeParams.hint_color || '#707579'); + root.style.setProperty('--tg-border-color', themeParams.border_color || themeParams.hint_color || '#c8c7cc'); + root.style.setProperty('--tg-separator-color', themeParams.separator_color || themeParams.hint_color || '#e5e5e5'); + } + + function initApp() { + tg.ready(); + tg.expand(); + applyTheme(tg.themeParams); + tg.onEvent('themeChanged', () => applyTheme(tg.themeParams)); + + if (tg.initDataUnsafe && tg.initDataUnsafe.user) { + currentUser = tg.initDataUnsafe.user; + document.getElementById('userDisplay').textContent = `Hi, ${currentUser.first_name}!`; + } else { + document.getElementById('userDisplay').textContent = 'Welcome!'; + // Potentially disable creation if no user + } + + document.querySelectorAll('.tab-button').forEach(button => { + button.addEventListener('click', () => switchTab(button.dataset.tab)); + }); + document.getElementById('fabCreateNew').addEventListener('click', showCreateForm); + document.getElementById('itemForm').addEventListener('submit', handleFormSubmit); + document.getElementById('cancelFormBtn').addEventListener('click', hideFormModal); + + document.getElementById('formModal').addEventListener('click', (event) => { + if (event.target === document.getElementById('formModal')) { + hideFormModal(); + } + }); + document.getElementById('detailModal').addEventListener('click', (event) => { + if (event.target === document.getElementById('detailModal')) { + hideDetailModal(); + } + }); + + tg.MainButton.setText('Create New'); + tg.MainButton.onClick(showCreateForm); + tg.MainButton.show(); -# --- Helper function to get item type display name --- -def get_item_type_display_name(item_type_slug): - names = { - 'resume': 'Resume', - 'vacancy': 'Vacancy', - 'freelance_offer': 'Freelance Offer' - } - return names.get(item_type_slug, 'Item') + loadItems(currentTab); + } -def get_item_collection_name(item_type_slug): - names = { - 'resume': 'resumes', - 'vacancy': 'vacancies', - 'freelance_offer': 'freelance_offers' - } - return names.get(item_type_slug) + function switchTab(tabName) { + currentTab = tabName; + document.querySelectorAll('.tab-button').forEach(button => { + button.classList.toggle('active', button.dataset.tab === tabName); + }); + tg.MainButton.setText(`Create New ${capitalizeFirstLetter(tabName.slice(0,-1))}`); + loadItems(tabName); + } + + function capitalizeFirstLetter(string) { + return string.charAt(0).toUpperCase() + string.slice(1); + } -# --- Flask Routes --- -@app.before_request -def check_auth_for_publish(): - if request.endpoint in ['publish_item', 'edit_item', 'delete_item', 'my_postings']: - if 'user' not in session or not session['user']: - flash("You need to be authenticated to access this page. Please ensure you are opening this app through Telegram.", "error") - return redirect(url_for('index')) - if not session['user'].get('id'): - flash("Authentication error: User ID missing.", "error") - session.pop('user', None) - return redirect(url_for('index')) + async function loadItems(type) { + const contentArea = document.getElementById('contentArea'); + contentArea.innerHTML = '
Loading...
'; + try { + const response = await fetch(`/api/${type}`); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + const items = await response.json(); + itemsCache[type] = items; + renderItems(type); + } catch (error) { + console.error('Error loading items:', error); + contentArea.innerHTML = '
Error loading items. Please try again.
'; + tg.showAlert(`Error loading ${type}: ${error.message}`); + } + } -@app.route('/auth_telegram', methods=['POST']) -def auth_telegram(): - data = request.get_json() - init_data_str = data.get('init_data') - if not init_data_str: - return jsonify({"status": "error", "message": "No initData received"}), 400 + function renderItems(type) { + const contentArea = document.getElementById('contentArea'); + const items = itemsCache[type]; + if (items.length === 0) { + contentArea.innerHTML = `
No ${type} found.
`; + return; + } + contentArea.innerHTML = items.map(item => ` +
+

${escapeHtml(item.title || item.job_title || item.service_title || 'Untitled')}

+

${escapeHtml(item.description ? item.description.substring(0, 100) + '...' : 'No description')}

+ +
+ `).join(''); + + document.querySelectorAll('.list-item').forEach(itemEl => { + itemEl.addEventListener('click', () => showItemDetail(itemEl.dataset.id, itemEl.dataset.type)); + }); + } + + function escapeHtml(unsafe) { + if (unsafe === null || typeof unsafe === 'undefined') return ''; + return String(unsafe) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } - user_info = validate_telegram_data(init_data_str) - - if user_info and user_info.get('id'): - session['user'] = user_info - logging.info(f"User {user_info.get('id')} authenticated via Telegram.") - return jsonify({"status": "success", "user": user_info}) - else: - logging.warning(f"Telegram authentication failed for initData: {init_data_str[:100]}...") - session.pop('user', None) - return jsonify({"status": "error", "message": "Invalid Telegram data or hash mismatch"}), 403 + function showCreateForm() { + document.getElementById('itemForm').reset(); + document.getElementById('itemId').value = ''; + document.getElementById('itemType').value = currentTab; + document.getElementById('formTitle').textContent = `Create New ${capitalizeFirstLetter(currentTab.slice(0,-1))}`; + + const allFormGroups = document.querySelectorAll('#itemForm .form-group'); + allFormGroups.forEach(group => group.style.display = 'none'); + + const fieldsForType = formFieldsConfig[currentTab]; + fieldsForType.forEach(fieldKey => { + const group = document.getElementById(`group-${fieldKey}`); + if (group) group.style.display = 'block'; + }); + + document.getElementById('formModal').classList.add('visible'); + tg.MainButton.hide(); + } -@app.route('/') -def index(): - data = load_data() - # Apply theme params from session if available, otherwise use defaults - theme_params = session.get('themeParams', {}) - return render_template_string( - BASE_TEMPLATE.format( - background_color=theme_params.get('bg_color'), - text_color=theme_params.get('text_color'), - hint_color=theme_params.get('hint_color'), - link_color=theme_params.get('link_color'), - button_color=theme_params.get('button_color'), - button_text_color=theme_params.get('button_text_color'), - secondary_bg_color=theme_params.get('secondary_bg_color'), - header_color=theme_params.get('header_bg_color'), - accent_text_color=theme_params.get('accent_text_color'), - section_bg_color=theme_params.get('section_bg_color'), - section_header_text_color=theme_params.get('section_header_text_color'), - destructive_text_color=theme_params.get('destructive_text_color') - ) + INDEX_TEMPLATE, - resumes=data.get('resumes', {}), - vacancies=data.get('vacancies', {}), - freelance_offers=data.get('freelance_offers', {}), - repo_id=REPO_ID - ) + function hideFormModal() { + document.getElementById('formModal').classList.remove('visible'); + tg.MainButton.show(); + } -@app.route('/publish/', methods=['GET', 'POST']) -@app.route('/edit//', methods=['GET', 'POST']) -def publish_item(item_type, item_id=None): - data = load_data() - collection_name = get_item_collection_name(item_type) - if not collection_name: - flash("Invalid item type.", "error") - return redirect(url_for('index')) + async function handleFormSubmit(event) { + event.preventDefault(); + if (!currentUser) { + tg.showAlert('You need to be logged in via Telegram to post.'); + return; + } - item_data_to_edit = None - if item_id: - item_data_to_edit = data.get(collection_name, {}).get(item_id) - if not item_data_to_edit or item_data_to_edit.get('user_id') != session['user']['id']: - flash("Item not found or you don't have permission to edit it.", "error") - return redirect(url_for('index')) - # Prepare complex fields for form display - if item_type == 'resume': - if item_data_to_edit.get('experience'): - item_data_to_edit['experience_str'] = "\n".join([f"{e['company']};{e['role']};{e['duration']};{e['description']}" for e in item_data_to_edit['experience']]) - if item_data_to_edit.get('education'): - item_data_to_edit['education_str'] = "\n".join([f"{e['institution']};{e['degree']};{e['duration']}" for e in item_data_to_edit['education']]) + const form = event.target; + const formData = new FormData(form); + const itemData = Object.fromEntries(formData.entries()); + + itemData.user_telegram_id = currentUser.id; + itemData.user_telegram_username = currentUser.username || `${currentUser.first_name}${currentUser.last_name ? ' ' + currentUser.last_name : ''}`; + + if (itemData.skills) itemData.skills = itemData.skills.split(',').map(s => s.trim()).filter(s => s); + + const type = itemData.type; + delete itemData.type; + + const itemId = itemData.id; + delete itemData.id; + + const method = itemId ? 'PUT' : 'POST'; + const url = itemId ? `/api/${type}/${itemId}` : `/api/${type}`; + + try { + const response = await fetch(url, { + method: method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(itemData) + }); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `HTTP error! status: ${response.status}`); + } + const savedItem = await response.json(); + hideFormModal(); + tg.HapticFeedback.notificationOccurred('success'); + tg.showAlert(`${capitalizeFirstLetter(type.slice(0,-1))} ${itemId ? 'updated' : 'created'} successfully!`); + loadItems(type); + } catch (error) { + console.error('Error submitting form:', error); + tg.showAlert(`Error: ${error.message}`); + tg.HapticFeedback.notificationOccurred('error'); + } + } + + function showItemDetail(itemId, itemType) { + const item = itemsCache[itemType].find(i => i.id === itemId); + if (!item) { + tg.showAlert('Item not found.'); + return; + } + + let detailHtml = `
`; + detailHtml += `

${escapeHtml(item.title || item.job_title || item.service_title || 'Untitled')}

`; + + if (item.company_name) detailHtml += `

Company: ${escapeHtml(item.company_name)}

`; + if (item.location) detailHtml += `

Location: ${escapeHtml(item.location)}

`; + + detailHtml += `

Description:
${escapeHtml(item.description).replace(/\\n/g, '
')}

`; + + if (item.skills && item.skills.length > 0) detailHtml += `

Skills: ${escapeHtml(Array.isArray(item.skills) ? item.skills.join(', ') : item.skills)}

`; + if (item.experience) detailHtml += `

Experience:
${escapeHtml(item.experience).replace(/\\n/g, '
')}

`; + if (item.education) detailHtml += `

Education:
${escapeHtml(item.education).replace(/\\n/g, '
')}

`; + if (item.requirements) detailHtml += `

Requirements:
${escapeHtml(item.requirements).replace(/\\n/g, '
')}

`; + if (item.salary_range) detailHtml += `

Salary/Budget: ${escapeHtml(item.salary_range)}

`; + if (item.timeline) detailHtml += `

Timeline: ${escapeHtml(item.timeline)}

`; + + detailHtml += `

Contact: ${escapeHtml(item.contact_info)}

`; + detailHtml += `

Posted by: ${escapeHtml(item.user_telegram_username || 'Anonymous')} on ${new Date(item.created_at).toLocaleString()}

`; + + if (currentUser && item.user_telegram_id === currentUser.id) { + detailHtml += ``; + detailHtml += ``; + } + detailHtml += ``; + detailHtml += `
`; - if request.method == 'POST': - new_item_id = item_id or str(uuid.uuid4()) - timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - - current_photo_filename = None - if item_data_to_edit: - if item_type == 'resume': current_photo_filename = item_data_to_edit.get('photo_filename') - elif item_type == 'vacancy': current_photo_filename = item_data_to_edit.get('company_logo_filename') + document.getElementById('detailContent').innerHTML = detailHtml; + document.getElementById('detailModal').classList.add('visible'); + tg.MainButton.hide(); + } - photo_file = request.files.get('photo') - uploaded_photo_filename = None + function hideDetailModal() { + document.getElementById('detailModal').classList.remove('visible'); + tg.MainButton.show(); + } - if photo_file and photo_file.filename: - if not HF_TOKEN_WRITE: - flash("Cannot upload photo: Hugging Face Write Token is not configured.", "warning") - else: - try: - uploads_dir = 'uploads_temp' - os.makedirs(uploads_dir, exist_ok=True) - api = HfApi() - - safe_name_prefix = secure_filename(request.form.get('full_name', request.form.get('company_name', request.form.get('title', 'item'))).replace(' ', '_'))[:20] - ext = os.path.splitext(photo_file.filename)[1].lower() - if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: - flash(f"File {photo_file.filename} is not a supported image format and was skipped.", "warning") - else: - new_photo_filename_base = f"{safe_name_prefix}_{new_item_id[:8]}_{timestamp.replace(':','-').replace(' ','_')}{ext}" - temp_path = os.path.join(uploads_dir, new_photo_filename_base) - photo_file.save(temp_path) - - api.upload_file( - path_or_fileobj=temp_path, - path_in_repo=f"photos/{new_photo_filename_base}", - repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, - commit_message=f"Upload photo for {item_type} {new_item_id}" - ) - uploaded_photo_filename = new_photo_filename_base - logging.info(f"Photo {uploaded_photo_filename} uploaded for {item_type} {new_item_id}.") - os.remove(temp_path) - if os.path.exists(uploads_dir) and not os.listdir(uploads_dir): os.rmdir(uploads_dir) - - # Delete old photo if a new one is uploaded and an old one existed - if current_photo_filename and uploaded_photo_filename != current_photo_filename: - try: - api.delete_file(repo_id=REPO_ID, path_in_repo=f"photos/{current_photo_filename}", repo_type="dataset", token=HF_TOKEN_WRITE) - logging.info(f"Old photo {current_photo_filename} deleted from HF.") - except Exception as e_del: - logging.error(f"Failed to delete old photo {current_photo_filename}: {e_del}") - except Exception as e: - logging.error(f"Error uploading photo: {e}", exc_info=True) - flash("Error uploading photo.", "error") + function editItem(itemId, itemType) { + const item = itemsCache[itemType].find(i => i.id === itemId); + if (!item) return; + hideDetailModal(); + document.getElementById('itemForm').reset(); + document.getElementById('itemId').value = item.id; + document.getElementById('itemType').value = itemType; + document.getElementById('formTitle').textContent = `Edit ${capitalizeFirstLetter(itemType.slice(0,-1))}`; + + const allFormGroups = document.querySelectorAll('#itemForm .form-group'); + allFormGroups.forEach(group => group.style.display = 'none'); + + const fieldsForType = formFieldsConfig[itemType]; + fieldsForType.forEach(fieldKey => { + const group = document.getElementById(`group-${fieldKey}`); + if (group) group.style.display = 'block'; + + const inputElement = document.getElementById(fieldKey); + if (inputElement && item[fieldKey] !== undefined) { + if (fieldKey === 'skills' && Array.isArray(item[fieldKey])) { + inputElement.value = item[fieldKey].join(', '); + } else { + inputElement.value = item[fieldKey]; + } + } + }); + + document.getElementById('formModal').classList.add('visible'); + } - item_details = { - 'id': new_item_id, - 'user_id': session['user']['id'], - 'user_display_name': f"{session['user'].get('first_name','')} {session['user'].get('last_name','')} ({session['user'].get('username','N/A')})".strip(), - 'published_at': item_data_to_edit.get('published_at', timestamp) if item_id else timestamp, - 'updated_at': timestamp + async function deleteItem(itemId, itemType) { + tg.showConfirm(`Are you sure you want to delete this ${itemType.slice(0,-1)}?`, async (confirmed) => { + if (confirmed) { + try { + const response = await fetch(`/api/${itemType}/${itemId}`, { method: 'DELETE' }); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `HTTP error! status: ${response.status}`); + } + tg.HapticFeedback.notificationOccurred('success'); + tg.showAlert('Item deleted successfully.'); + hideDetailModal(); + loadItems(itemType); + } catch (error) { + console.error('Error deleting item:', error); + tg.showAlert(`Error deleting item: ${error.message}`); + tg.HapticFeedback.notificationOccurred('error'); + } + } + }); } - if item_type == 'resume': - item_details.update({ - 'full_name': request.form.get('full_name'), - 'title': request.form.get('title'), - 'skills': [s.strip() for s in request.form.get('skills', '').split(',') if s.strip()], - 'contact_info': request.form.get('contact_info'), - 'portfolio_links': [l.strip() for l in request.form.get('portfolio_links', '').split(',') if l.strip()], - 'photo_filename': uploaded_photo_filename or current_photo_filename - }) - exp_text = request.form.get('experience', '') - item_details['experience'] = [] - for line in exp_text.splitlines(): - parts = [p.strip() for p in line.split(';')] - if len(parts) == 4: item_details['experience'].append({'company': parts[0], 'role': parts[1], 'duration': parts[2], 'description': parts[3]}) - - edu_text = request.form.get('education', '') - item_details['education'] = [] - for line in edu_text.splitlines(): - parts = [p.strip() for p in line.split(';')] - if len(parts) == 3: item_details['education'].append({'institution': parts[0], 'degree': parts[1], 'duration': parts[2]}) + document.addEventListener('DOMContentLoaded', initApp); + + + +""" - elif item_type == 'vacancy': - item_details.update({ - 'company_name': request.form.get('company_name'), - 'job_title': request.form.get('job_title'), - 'description': request.form.get('description'), - 'requirements': [r.strip() for r in request.form.get('requirements', '').split(',') if r.strip()], - 'location': request.form.get('location'), - 'salary_range': request.form.get('salary_range'), - 'employment_type': request.form.get('employment_type'), - 'contact_info': request.form.get('contact_info'), - 'company_logo_filename': uploaded_photo_filename or current_photo_filename - }) - elif item_type == 'freelance_offer': - item_details.update({ - 'title': request.form.get('title'), - 'description': request.form.get('description'), - 'skills_required': [s.strip() for s in request.form.get('skills_required', '').split(',') if s.strip()], - 'budget': request.form.get('budget'), - 'deadline': request.form.get('deadline'), - 'contact_info': request.form.get('contact_info') - }) - - data.setdefault(collection_name, {})[new_item_id] = item_details - save_data(data) - flash(f"{get_item_type_display_name(item_type)} {'updated' if item_id else 'published'} successfully!", "success") - return redirect(url_for('view_item', item_type=item_type, item_id=new_item_id)) +@app.route('/') +def index(): + return render_template_string(APP_HTML_TEMPLATE) - return render_template_string( - BASE_TEMPLATE.format(**session.get('themeParams', {})) + PUBLISH_ITEM_TEMPLATE, # Pass empty dict if no themeParams - item_type=item_type, - item_type_display_name=get_item_type_display_name(item_type), - item_data=item_data_to_edit, - repo_id=REPO_ID - ) +def get_entity_type_from_request(item_type_plural): + if item_type_plural not in ['resumes', 'vacancies', 'freelance_offers']: + return None, "Invalid item type" + return item_type_plural, None -@app.route('/view//') -def view_item(item_type, item_id): +@app.route('/api/', methods=['GET']) +def get_items(item_type_plural): + entity_type, error = get_entity_type_from_request(item_type_plural) + if error: return jsonify({"error": error}), 400 + data = load_data() - collection_name = get_item_collection_name(item_type) - if not collection_name: - flash("Invalid item type.", "error") - return redirect(url_for('index')) + items = sorted(data.get(entity_type, []), key=lambda x: x.get('created_at', ''), reverse=True) + return jsonify(items) - item = data.get(collection_name, {}).get(item_id) - if not item: - flash("Item not found.", "error") - return redirect(url_for('index')) +@app.route('/api/', methods=['POST']) +def create_item(item_type_plural): + entity_type, error = get_entity_type_from_request(item_type_plural) + if error: return jsonify({"error": error}), 400 - return render_template_string( - BASE_TEMPLATE.format(**session.get('themeParams', {})) + VIEW_ITEM_TEMPLATE, - item_type=item_type, - item_id=item_id, - item=item, - repo_id=REPO_ID - ) + item_data = request.get_json() + if not item_data: return jsonify({"error": "No data provided"}), 400 + if not item_data.get('user_telegram_id'): return jsonify({"error": "User information missing"}), 400 -@app.route('/delete//', methods=['POST']) -def delete_item(item_type, item_id): data = load_data() - collection_name = get_item_collection_name(item_type) - if not collection_name: - flash("Invalid item type.", "error") - return redirect(url_for('index')) - - item_to_delete = data.get(collection_name, {}).get(item_id) - if not item_to_delete or item_to_delete.get('user_id') != session['user']['id']: - flash("Item not found or you don't have permission to delete it.", "error") - return redirect(url_for('index')) - - photo_filename_key = None - if item_type == 'resume': photo_filename_key = 'photo_filename' - elif item_type == 'vacancy': photo_filename_key = 'company_logo_filename' + item_data['id'] = str(uuid.uuid4()) + item_data['created_at'] = datetime.utcnow().isoformat() + "Z" - old_photo_to_delete = item_to_delete.get(photo_filename_key) if photo_filename_key else None + if 'skills' in item_data and isinstance(item_data['skills'], str): # From older form submissions if any + item_data['skills'] = [s.strip() for s in item_data['skills'].split(',') if s.strip()] - del data[collection_name][item_id] + data[entity_type].append(item_data) save_data(data) + return jsonify(item_data), 201 + +@app.route('/api//', methods=['GET']) +def get_item(item_type_plural, item_id): + entity_type, error = get_entity_type_from_request(item_type_plural) + if error: return jsonify({"error": error}), 400 - if old_photo_to_delete and HF_TOKEN_WRITE: - try: - api = HfApi() - api.delete_file(repo_id=REPO_ID, path_in_repo=f"photos/{old_photo_to_delete}", repo_type="dataset", token=HF_TOKEN_WRITE) - logging.info(f"Photo {old_photo_to_delete} deleted from HF for deleted {item_type} {item_id}.") - except Exception as e: - logging.error(f"Error deleting photo {old_photo_to_delete} from HF: {e}", exc_info=True) - flash("Item deleted, but failed to delete associated photo from storage.", "warning") + data = load_data() + item = next((i for i in data.get(entity_type, []) if i['id'] == item_id), None) + if item: return jsonify(item) + return jsonify({"error": "Item not found"}), 404 - flash(f"{get_item_type_display_name(item_type)} deleted successfully.", "success") - return redirect(url_for('my_postings')) +@app.route('/api//', methods=['PUT']) +def update_item(item_type_plural, item_id): + entity_type, error = get_entity_type_from_request(item_type_plural) + if error: return jsonify({"error": error}), 400 + + updated_data = request.get_json() + if not updated_data: return jsonify({"error": "No data provided"}), 400 + + # Ensure user_telegram_id from request matches the one attempting to update + # This is a basic check; proper auth would involve validating Telegram's initData + requesting_user_id = updated_data.pop('user_telegram_id', None) + # username is not strictly needed for update logic but good to keep consistent + updated_data.pop('user_telegram_username', None) -@app.route('/my_postings') -def my_postings(): data = load_data() - user_id = session['user']['id'] - - my_resumes = {k: v for k, v in data.get('resumes', {}).items() if v.get('user_id') == user_id} - my_vacancies = {k: v for k, v in data.get('vacancies', {}).items() if v.get('user_id') == user_id} - my_freelance_offers = {k: v for k, v in data.get('freelance_offers', {}).items() if v.get('user_id') == user_id} + items_list = data.get(entity_type, []) + item_index = -1 + for i, item_in_list in enumerate(items_list): + if item_in_list['id'] == item_id: + item_index = i + break + + if item_index == -1: return jsonify({"error": "Item not found"}), 404 - return render_template_string( - BASE_TEMPLATE.format(**session.get('themeParams', {})) + MY_POSTINGS_TEMPLATE, - my_resumes=my_resumes, - my_vacancies=my_vacancies, - my_freelance_offers=my_freelance_offers, - repo_id=REPO_ID - ) + original_item = items_list[item_index] + if str(original_item.get('user_telegram_id')) != str(requesting_user_id): + return jsonify({"error": "Unauthorized to edit this item"}), 403 -@app.route('/admin_sync', methods=['GET']) -def admin_sync(): - # Basic protection: only allow if user is authenticated (any Telegram user) - # For real admin, you'd check against a list of admin user IDs - if 'user' not in session or not session['user']: - flash("You must be authenticated to access the sync panel.", "error") - return redirect(url_for('index')) - + if 'skills' in updated_data and isinstance(updated_data['skills'], str): + updated_data['skills'] = [s.strip() for s in updated_data['skills'].split(',') if s.strip()] + + # Preserve original creation date and ID, user info + updated_data['id'] = original_item['id'] + updated_data['created_at'] = original_item['created_at'] + updated_data['user_telegram_id'] = original_item['user_telegram_id'] + updated_data['user_telegram_username'] = original_item['user_telegram_username'] + + items_list[item_index] = updated_data + data[entity_type] = items_list + save_data(data) + return jsonify(updated_data) + +@app.route('/api//', methods=['DELETE']) +def delete_item(item_type_plural, item_id): + entity_type, error = get_entity_type_from_request(item_type_plural) + if error: return jsonify({"error": error}), 400 + + # For deletion, we'd typically verify ownership on the server-side. + # This requires securely getting the authenticated user ID from the request. + # For Telegram Mini Apps, this means validating initData or a session. + # For simplicity here, we'll allow deletion if ID matches, but in prod, validate owner. + # Example: user_id_from_validated_telegram_auth = validate_auth_header(request.headers) + data = load_data() - data_counts = { - "resumes": len(data.get('resumes', {})), - "vacancies": len(data.get('vacancies', {})), - "freelance_offers": len(data.get('freelance_offers', {})) - } - return render_template_string( - BASE_TEMPLATE.format(**session.get('themeParams', {})) + ADMIN_SYNC_TEMPLATE, - data_counts=data_counts - ) + items_list = data.get(entity_type, []) + original_length = len(items_list) + + # Find the item to verify ownership if we had secure auth + # item_to_delete = next((item for item in items_list if item['id'] == item_id), None) + # if item_to_delete and str(item_to_delete.get('user_telegram_id')) != str(user_id_from_validated_telegram_auth): + # return jsonify({"error": "Unauthorized to delete this item"}), 403 -@app.route('/force_upload_sync', methods=['POST']) # Renamed route to avoid conflict if old code is around -def force_upload(): - if 'user' not in session or not session['user']: # Basic protection - flash("Authentication required.", "error") - return redirect(url_for('index')) - logging.info("Forcing upload to Hugging Face...") - upload_db_to_hf() - flash("Data successfully uploaded to Hugging Face.", 'success') - return redirect(url_for('admin_sync')) + new_items_list = [item for item in items_list if item['id'] != item_id] + + if len(new_items_list) == original_length: + return jsonify({"error": "Item not found"}), 404 + + data[entity_type] = new_items_list + save_data(data) + return jsonify({"message": "Item deleted successfully"}), 200 -@app.route('/force_download_sync', methods=['POST']) # Renamed route -def force_download(): - if 'user' not in session or not session['user']: # Basic protection - flash("Authentication required.", "error") - return redirect(url_for('index')) - logging.info("Forcing download from Hugging Face...") - if download_db_from_hf(): - flash("Data successfully downloaded. Local files updated.", 'success') - load_data() # Reload data in memory - else: - flash("Failed to download data. Check logs.", 'error') - return redirect(url_for('admin_sync')) -# --- App Initialization --- if __name__ == '__main__': - logging.info("TonTalent Application starting up...") - if not os.path.exists(TONTALENT_DATA_FILE): - logging.info(f"{TONTALENT_DATA_FILE} not found locally, attempting initial download/creation.") - download_db_from_hf() # This will create an empty file if not on HF and not local - - load_data() # Load initial data - logging.info("Initial data load/check complete.") + logging.info("Application starting up for TonTalent...") + if HF_TOKEN_WRITE or HF_TOKEN_READ: # Only attempt HF sync if tokens are present + download_db_from_hf() # Initial download attempt + load_data() # Load local or downloaded data + logging.info("Initial data load complete.") if HF_TOKEN_WRITE: backup_thread = threading.Thread(target=periodic_backup, daemon=True) @@ -1224,6 +951,6 @@ if __name__ == '__main__': else: logging.warning("Periodic backup will NOT run (HF_TOKEN_WRITE not set).") - port = int(os.environ.get('PORT', 7861)) # Different port from original app + port = int(os.environ.get('PORT', 7860)) logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}") app.run(debug=False, host='0.0.0.0', port=port) \ No newline at end of file