diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,3 +1,4 @@ + #!/usr/bin/env python3 import os @@ -7,12 +8,13 @@ import hashlib import json from urllib.parse import unquote, parse_qs, quote import time -from datetime import datetime, timezone +from datetime import datetime import logging import threading import random from huggingface_hub import HfApi, hf_hub_download from huggingface_hub.utils import RepositoryNotFoundError +import pytz BOT_TOKEN = os.getenv("BOT_TOKEN", "7835463659:AAGNePbelZIAOeaglyQi1qulOqnjs4BGQn4") HOST = '0.0.0.0' @@ -24,17 +26,15 @@ HF_DATA_FILE_PATH = "data.json" HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") +BISHKEK_TZ = pytz.timezone('Asia/Bishkek') + app = Flask(__name__) logging.basicConfig(level=logging.INFO) app.secret_key = os.urandom(24) _data_lock = threading.Lock() visitor_data_cache = {} - -KYRGYZSTAN_TIMEZONE = timezone(timedelta(hours=6)) # UTC+6 for Bishkek - -def get_current_time_kyrgyzstan(): - return datetime.now(KYRGYZSTAN_TIMEZONE) +org_contact_info_cache = {} def generate_unique_id(all_data): while True: @@ -43,7 +43,7 @@ def generate_unique_id(all_data): return new_id def download_data_from_hf(): - global visitor_data_cache + global visitor_data_cache, org_contact_info_cache if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ not set. Skipping Hugging Face download.") return False @@ -63,11 +63,14 @@ def download_data_from_hf(): with _data_lock: try: with open(DATA_FILE, 'r', encoding='utf-8') as f: - visitor_data_cache = json.load(f) + full_data = json.load(f) + visitor_data_cache = full_data.get('users', {}) + org_contact_info_cache = full_data.get('org_contact_info', {}) logging.info("Successfully loaded downloaded data into cache.") except (FileNotFoundError, json.JSONDecodeError) as e: logging.error(f"Error reading downloaded data file: {e}. Starting with empty cache.") visitor_data_cache = {} + org_contact_info_cache = {} return True except RepositoryNotFoundError: logging.error(f"Hugging Face repository '{REPO_ID}' not found. Cannot download data.") @@ -75,35 +78,60 @@ def download_data_from_hf(): logging.error(f"Error downloading data from Hugging Face: {e}") return False -def load_visitor_data(): - global visitor_data_cache +def load_full_data(): + global visitor_data_cache, org_contact_info_cache with _data_lock: - if not visitor_data_cache: - try: - with open(DATA_FILE, 'r', encoding='utf-8') as f: - visitor_data_cache = json.load(f) - logging.info("Visitor data loaded from local JSON.") - except FileNotFoundError: - logging.warning(f"{DATA_FILE} not found locally. Starting with empty data.") - visitor_data_cache = {} - except json.JSONDecodeError: - logging.error(f"Error decoding {DATA_FILE}. Starting with empty data.") - visitor_data_cache = {} - except Exception as e: - logging.error(f"Unexpected error loading visitor data: {e}") - visitor_data_cache = {} - return visitor_data_cache + # Check cache first + if visitor_data_cache or org_contact_info_cache: + return {'users': visitor_data_cache, 'org_contact_info': org_contact_info_cache} -def save_visitor_data(data): + try: + with open(DATA_FILE, 'r', encoding='utf-8') as f: + full_data = json.load(f) + visitor_data_cache = full_data.get('users', {}) + org_contact_info_cache = full_data.get('org_contact_info', {}) + logging.info("Full data loaded from local JSON.") + except FileNotFoundError: + logging.warning(f"{DATA_FILE} not found locally. Starting with empty data.") + visitor_data_cache = {} + org_contact_info_cache = {} + except json.JSONDecodeError: + logging.error(f"Error decoding {DATA_FILE}. Starting with empty data.") + visitor_data_cache = {} + org_contact_info_cache = {} + except Exception as e: + logging.error(f"Unexpected error loading full data: {e}") + visitor_data_cache = {} + org_contact_info_cache = {} + + return {'users': visitor_data_cache, 'org_contact_info': org_contact_info_cache} + + +def save_full_data(): with _data_lock: try: - visitor_data_cache.update(data) + full_data_to_save = { + 'users': visitor_data_cache, + 'org_contact_info': org_contact_info_cache + } with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4) - logging.info(f"Visitor data successfully saved to {DATA_FILE}.") + json.dump(full_data_to_save, f, ensure_ascii=False, indent=4) + logging.info(f"Full data successfully saved to {DATA_FILE}.") upload_data_to_hf_async() except Exception as e: - logging.error(f"Error saving visitor data: {e}") + logging.error(f"Error saving full data: {e}") + + +def save_visitor_data(data): + with _data_lock: + visitor_data_cache.update(data) + save_full_data() + +def save_org_contact_info(info): + global org_contact_info_cache + with _data_lock: + org_contact_info_cache = info + save_full_data() def upload_data_to_hf(): if not HF_TOKEN_WRITE: @@ -128,7 +156,7 @@ def upload_data_to_hf(): repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, - commit_message=f"Update bonus data {get_current_time_kyrgyzstan().strftime('%Y-%m-%d %H:%M:%S')}" + commit_message=f"Update bonus data {datetime.now(BISHKEK_TZ).strftime('%Y-%m-%d %H:%M:%S')}" ) logging.info("Bonus data successfully uploaded to Hugging Face.") except Exception as e: @@ -145,7 +173,7 @@ def periodic_backup(): while True: time.sleep(3600) logging.info("Initiating periodic backup...") - upload_data_to_hf() + save_full_data() # Save locally and trigger HF upload def verify_telegram_data(init_data_str): try: @@ -168,6 +196,8 @@ def verify_telegram_data(init_data_str): current_time = int(time.time()) if current_time - auth_date > 86400: logging.warning(f"Telegram InitData is older than 24 hours (Auth Date: {auth_date}, Current: {current_time}).") + # Decide if you want to fail verification for old data + # return parsed_data, False # Uncomment to fail old data return parsed_data, True else: logging.warning(f"Data verification failed. Calculated: {calculated_hash}, Received: {received_hash}") @@ -204,6 +234,7 @@ TEMPLATE = """ --shadow-glow: 0 0 35px var(--shadow-color); --shadow-color-red: rgba(244, 67, 54, 0.15); --shadow-glow-red: 0 0 35px var(--shadow-color-red); + --section-bg: #2c2c2e; } * { box-sizing: border-box; margin: 0; padding: 0; } html, body { @@ -214,7 +245,7 @@ TEMPLATE = """ overscroll-behavior-y: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - visibility: hidden; + visibility: hidden; /* Hide until theme applied or loaded */ min-height: 100vh; } .container { @@ -261,21 +292,21 @@ TEMPLATE = """ box-shadow: var(--shadow-glow-red); border: 1px solid rgba(244, 67, 54, 0.2); } - .card-label { - font-size: 1.1em; + .card-label { + font-size: 1em; font-weight: 500; color: var(--text-secondary-color); margin-bottom: 12px; } .bonus-amount { - font-size: 3em; + font-size: 2.5em; font-weight: 800; color: var(--brand-yellow); letter-spacing: -2px; line-height: 1; } .debt-amount { - font-size: 3em; + font-size: 2.5em; font-weight: 800; color: var(--brand-red); letter-spacing: -2px; @@ -292,68 +323,71 @@ TEMPLATE = """ .client-id-label { font-weight: 500; color: var(--text-secondary-color); + font-size: 0.9em; } .client-id-value { - font-size: 1.3em; + font-size: 1.1em; font-weight: 700; color: var(--brand-yellow); - letter-spacing: 2px; + letter-spacing: 1px; background-color: rgba(255,193,7,0.1); - padding: 4px 10px; + padding: 4px 8px; border-radius: 8px; } - .navigation-buttons { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: var(--padding-m); - margin-top: var(--padding-m); + + .navigation { + display: flex; + justify-content: space-around; + gap: 10px; + margin-bottom: var(--padding-m); } + .nav-button { + flex-grow: 1; + padding: 12px; background-color: var(--card-bg); + color: var(--text-color); + border: none; border-radius: var(--border-radius); - padding: var(--padding-m); - text-align: center; + font-size: 0.9em; + font-weight: 600; cursor: pointer; - transition: background-color 0.2s ease, transform 0.2s ease; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 80px; - border: 1px solid rgba(255, 255, 255, 0.05); + transition: background-color 0.2s ease; + text-align: center; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); } .nav-button:hover { - background-color: #2a2a2c; - transform: translateY(-3px); - } - .nav-button-icon { - font-size: 1.8em; - margin-bottom: 8px; - color: var(--brand-yellow); + background-color: #3a3a3c; } - .nav-button-text { - font-size: 0.9em; - font-weight: 500; - color: var(--text-secondary-color); + .nav-button.active { + background-color: var(--brand-yellow); + color: var(--brand-black); } - .history-section { - background-color: var(--card-bg); + + .content-section { + background-color: var(--section-bg); border-radius: var(--border-radius); padding: var(--padding-l); - margin-top: var(--padding-m); + display: none; /* Hidden by default */ + } + .content-section.active { + display: block; } - .history-title { + + .section-title { font-size: 1.4em; font-weight: 700; margin-bottom: var(--padding-m); padding-bottom: var(--padding-m); border-bottom: 1px solid rgba(255, 255, 255, 0.1); } + + /* History Styles */ .history-list { list-style: none; padding: 0; margin: 0; - max-height: 35vh; + max-height: 40vh; /* Limit height for scrolling */ overflow-y: auto; } .history-item { @@ -364,110 +398,103 @@ TEMPLATE = """ border-bottom: 1px solid rgba(255, 255, 255, 0.05); } .history-item:last-child { border-bottom: none; } - .history-details { display: flex; flex-direction: column; } - .history-description { font-size: 1em; font-weight: 500; } + .history-details { display: flex; flex-direction: column; flex-grow: 1; margin-right: 10px;} + .history-description { font-size: 1em; font-weight: 500; word-break: break-word; } .history-date { font-size: 0.8em; color: var(--text-secondary-color); margin-top: 4px; } - .history-amount { font-size: 1.1em; font-weight: 700; } + .history-amount { font-size: 1.1em; font-weight: 700; flex-shrink: 0; } .history-amount.accrual { color: var(--brand-green); } .history-amount.deduction { color: var(--brand-red); } - .no-history { - text-align: center; - color: var(--text-secondary-color); - padding: 2rem 0; - } - .modal { - display: none; - position: fixed; - z-index: 1000; - left: 0; - top: 0; - width: 100%; - height: 100%; - overflow: auto; - background-color: rgba(0,0,0,0.5); - backdrop-filter: blur(5px); - -webkit-backdrop-filter: blur(5px); - padding: 20px; - box-sizing: border-box; + .history-amount.payment { color: var(--brand-green); } /* For debt payments */ + .history-amount.debt-accrual { color: var(--brand-red); } /* For adding debt */ + + /* Invoice Styles */ + .invoice-list { + list-style: none; + padding: 0; + margin: 0; + max-height: 40vh; /* Limit height for scrolling */ + overflow-y: auto; } - .modal-content { + .invoice-item { background-color: var(--card-bg); - margin: 20px auto; - padding: var(--padding-l); - border: 1px solid rgba(255, 255, 255, 0.05); border-radius: var(--border-radius); - width: 100%; - max-width: 700px; - box-shadow: 0 0 30px rgba(255, 193, 7, 0.1); + margin-bottom: var(--padding-m); + padding: var(--padding-m); + border: 1px solid rgba(255, 255, 255, 0.1); } - .modal-header { + .invoice-summary { display: flex; justify-content: space-between; align-items: center; - margin-bottom: var(--padding-m); - padding-bottom: var(--padding-m); - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - } - .modal-header h2 { - font-size: 1.5em; - font-weight: 700; - color: var(--brand-yellow); + margin-bottom: 10px; + cursor: pointer; /* Make summary clickable to toggle items */ + } + .invoice-summary .date { font-size: 0.9em; color: var(--text-secondary-color); } + .invoice-summary .total { font-size: 1.2em; font-weight: 700; color: var(--brand-yellow); } + .invoice-details { + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid rgba(255, 255, 255, 0.05); + display: none; /* Hidden by default */ + } + .invoice-details.expanded { display: block; } + .invoice-item-list { list-style: none; padding: 0; margin: 0; } + .invoice-line-item { + display: flex; + justify-content: space-between; + font-size: 0.9em; + padding: 5px 0; + border-bottom: 1px dashed rgba(255, 255, 255, 0.03); } - .modal-close { - font-size: 2em; - cursor: pointer; + .invoice-line-item:last-child { border-bottom: none; } + .invoice-line-item .item-name { flex-grow: 1; margin-right: 10px; } + .invoice-line-item .item-qty-price { flex-shrink: 0; text-align: right; color: var(--text-secondary-color); } + .no-items { + text-align: center; color: var(--text-secondary-color); - line-height: 1; - } - .modal-close:hover { - color: var(--text-color); + padding: 2rem 0; } - .invoice-section { - background-color: var(--card-bg); - border-radius: var(--border-radius); - padding: var(--padding-m); - margin-bottom: var(--padding-m); - border: 1px solid rgba(255, 255, 255, 0.05); + + /* Contact Info Styles */ + .contact-info-details { + display: flex; + flex-direction: column; + gap: 15px; } - .invoice-title { - font-size: 1.3em; - font-weight: 700; - margin-bottom: var(--padding-m); - color: var(--brand-yellow); + .contact-item { display: flex; - justify-content: space-between; align-items: center; + gap: 15px; + font-size: 1em; } - .invoice-date { - font-size: 0.9em; - color: var(--text-secondary-color); - } - .invoice-item { + .contact-item a { + color: var(--text-color); + text-decoration: none; display: flex; - justify-content: space-between; - padding: 10px 0; - border-bottom: 1px dashed rgba(255, 255, 255, 0.1); - font-size: 0.95em; + align-items: center; + gap: 10px; + word-break: break-word; + } + .contact-item a:hover { + color: var(--brand-yellow); } - .invoice-item:last-child { - border-bottom: none; + .contact-item svg { + width: 24px; + height: 24px; + fill: var(--brand-yellow); + flex-shrink: 0; } - .invoice-item .name { flex-grow: 1; margin-right: 10px; } - .invoice-item .quantity { min-width: 50px; text-align: right; margin-right: 10px; } - .invoice-item .price { min-width: 80px; text-align: right; } - .invoice-total { - text-align: right; + .org-name { font-size: 1.2em; font-weight: 700; - margin-top: var(--padding-m); - padding-top: var(--padding-m); - border-top: 1px solid rgba(255, 255, 255, 0.1); - } - .no-invoices { - text-align: center; - color: var(--text-secondary-color); - padding: 2rem 0; - } + margin-bottom: var(--padding-m); + } + .no-contact-info { + text-align: center; + color: var(--text-secondary-color); + padding: 2rem 0; + } +
@@ -493,26 +520,17 @@ TEMPLATE = """{{ user.id }}
- + -Операций пока не было.
+Операций пока не было.
{% endif %} -У вас пока нет накладных.
+ {% endif %} +Информация о компании пока не добавлена.
+ {% endif %} +