✅ Fixed Critical Issues: - Fixed dynamic QR code short URL redirect functionality - Resolved data consistency issues with multiple LinkPageManager instances - Fixed worker concurrency problems in Gunicorn configuration 🎨 UI/UX Enhancements: - Separated public page from admin statistics view - Created clean public_page.html for QR code users (no admin info) - Added comprehensive statistics_page.html for admin analytics - Enhanced dashboard with separate 'Manage' and 'Stats' buttons - Improved navigation flow throughout the application 🔧 Technical Improvements: - Added URLShortener instance reloading for data consistency - Reduced Gunicorn workers to 1 to prevent file conflicts - Increased timeout to 60s for better performance - Enhanced debug logging for troubleshooting - Added proper error handling and 404 responses 📁 New Files: - app/templates/public_page.html - Clean public interface - app/templates/statistics_page.html - Admin analytics dashboard �� Modified Files: - app/routes/main.py - Added /stats route, improved short URL handling - app/templates/edit_links.html - Added Statistics button - app/templates/index.html - Added Stats button for QR codes - app/utils/link_manager.py - Enhanced data reloading - app/utils/url_shortener.py - Added debug logging - gunicorn.conf.py - Optimized worker configuration This update provides a professional separation between public content and admin functionality while ensuring reliable short URL operation.
217 lines
8.8 KiB
Python
Executable File
217 lines
8.8 KiB
Python
Executable File
"""
|
|
Dynamic Link Page Manager utilities
|
|
"""
|
|
|
|
import uuid
|
|
import json
|
|
import os
|
|
from datetime import datetime
|
|
from .url_shortener import URLShortener
|
|
|
|
# Data storage directory
|
|
DATA_DIR = 'data'
|
|
LINK_PAGES_FILE = os.path.join(DATA_DIR, 'link_pages.json')
|
|
|
|
# Ensure data directory exists
|
|
os.makedirs(DATA_DIR, exist_ok=True)
|
|
|
|
class LinkPageManager:
|
|
def __init__(self):
|
|
self.url_shortener = URLShortener()
|
|
self.link_pages_db = self._load_link_pages()
|
|
|
|
def _load_link_pages(self):
|
|
"""Load link pages from JSON file"""
|
|
try:
|
|
if os.path.exists(LINK_PAGES_FILE):
|
|
with open(LINK_PAGES_FILE, 'r', encoding='utf-8') as f:
|
|
return json.load(f)
|
|
return {}
|
|
except Exception as e:
|
|
print(f"Error loading link pages: {e}")
|
|
return {}
|
|
|
|
def _save_link_pages(self):
|
|
"""Save link pages to JSON file"""
|
|
try:
|
|
with open(LINK_PAGES_FILE, 'w', encoding='utf-8') as f:
|
|
json.dump(self.link_pages_db, f, indent=2, ensure_ascii=False)
|
|
except Exception as e:
|
|
print(f"Error saving link pages: {e}")
|
|
|
|
def create_link_page(self, title="My Links", description="Collection of useful links"):
|
|
"""Create a new dynamic link page"""
|
|
page_id = str(uuid.uuid4())
|
|
page_data = {
|
|
'id': page_id,
|
|
'title': title,
|
|
'description': description,
|
|
'links': [],
|
|
'created_at': datetime.now().isoformat(),
|
|
'updated_at': datetime.now().isoformat(),
|
|
'view_count': 0,
|
|
'short_url': None, # Will be set when short URL is created
|
|
'short_code': None
|
|
}
|
|
self.link_pages_db[page_id] = page_data
|
|
self._save_link_pages() # Persist to file
|
|
return page_id
|
|
|
|
def set_page_short_url(self, page_id, short_url, short_code):
|
|
"""Set the short URL for a link page"""
|
|
if page_id in self.link_pages_db:
|
|
self.link_pages_db[page_id]['short_url'] = short_url
|
|
self.link_pages_db[page_id]['short_code'] = short_code
|
|
self.link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
|
self._save_link_pages() # Persist to file
|
|
return True
|
|
return False
|
|
|
|
def add_link(self, page_id, title, url, description="", enable_shortener=False, custom_short_code=None):
|
|
"""Add a link to a page with optional URL shortening"""
|
|
print(f"DEBUG: LinkManager.add_link called for page {page_id}")
|
|
print(f"DEBUG: Current pages in memory: {list(self.link_pages_db.keys())}")
|
|
|
|
if page_id not in self.link_pages_db:
|
|
print(f"DEBUG: Page {page_id} not found in memory, reloading from file")
|
|
self.link_pages_db = self._load_link_pages()
|
|
print(f"DEBUG: After reload, pages: {list(self.link_pages_db.keys())}")
|
|
|
|
if page_id not in self.link_pages_db:
|
|
print(f"DEBUG: Page {page_id} still not found after reload")
|
|
return False
|
|
|
|
# Ensure URL has protocol
|
|
if not url.startswith(('http://', 'https://')):
|
|
url = f'https://{url}'
|
|
|
|
# Create the link data
|
|
link_data = {
|
|
'id': str(uuid.uuid4()),
|
|
'title': title,
|
|
'url': url,
|
|
'description': description,
|
|
'created_at': datetime.now().isoformat(),
|
|
'short_url': None,
|
|
'short_code': None,
|
|
'clicks': 0
|
|
}
|
|
|
|
# Generate short URL if enabled
|
|
if enable_shortener:
|
|
print(f"DEBUG: Creating short URL for {url}")
|
|
try:
|
|
short_result = self.url_shortener.create_short_url(
|
|
url,
|
|
custom_code=custom_short_code,
|
|
title=title
|
|
)
|
|
print(f"DEBUG: Short URL created: {short_result}")
|
|
link_data['short_url'] = short_result['short_url']
|
|
link_data['short_code'] = short_result['short_code']
|
|
except Exception as e:
|
|
# If shortening fails, continue without it
|
|
print(f"DEBUG: URL shortening failed: {e}")
|
|
|
|
print(f"DEBUG: Adding link to page data: {link_data}")
|
|
self.link_pages_db[page_id]['links'].append(link_data)
|
|
self.link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
|
|
|
print(f"DEBUG: Saving link pages to file")
|
|
self._save_link_pages() # Persist to file
|
|
print(f"DEBUG: Link added successfully")
|
|
return True
|
|
|
|
def update_link(self, page_id, link_id, title=None, url=None, description=None, enable_shortener=None, custom_short_code=None):
|
|
"""Update a specific link with optional URL shortening"""
|
|
if page_id not in self.link_pages_db:
|
|
return False
|
|
|
|
for link in self.link_pages_db[page_id]['links']:
|
|
if link['id'] == link_id:
|
|
if title is not None:
|
|
link['title'] = title
|
|
if url is not None:
|
|
link['url'] = url if url.startswith(('http://', 'https://')) else f'https://{url}'
|
|
if description is not None:
|
|
link['description'] = description
|
|
|
|
# Handle URL shortening update
|
|
if enable_shortener is not None:
|
|
if enable_shortener and not link.get('short_url'):
|
|
# Create new short URL
|
|
try:
|
|
short_result = self.url_shortener.create_short_url(
|
|
link['url'],
|
|
custom_code=custom_short_code,
|
|
title=link['title']
|
|
)
|
|
link['short_url'] = short_result['short_url']
|
|
link['short_code'] = short_result['short_code']
|
|
except Exception as e:
|
|
print(f"URL shortening failed: {e}")
|
|
elif not enable_shortener and link.get('short_code'):
|
|
# Remove short URL
|
|
if link.get('short_code'):
|
|
self.url_shortener.delete_url(link['short_code'])
|
|
link['short_url'] = None
|
|
link['short_code'] = None
|
|
|
|
self.link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
|
self._save_link_pages() # Persist to file
|
|
return True
|
|
return False
|
|
|
|
def delete_link(self, page_id, link_id):
|
|
"""Delete a specific link"""
|
|
if page_id not in self.link_pages_db:
|
|
return False
|
|
|
|
links = self.link_pages_db[page_id]['links']
|
|
for link in links:
|
|
if link['id'] == link_id and link.get('short_code'):
|
|
# Delete the short URL if it exists
|
|
self.url_shortener.delete_url(link['short_code'])
|
|
|
|
self.link_pages_db[page_id]['links'] = [link for link in links if link['id'] != link_id]
|
|
self.link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
|
self._save_link_pages() # Persist to file
|
|
return True
|
|
|
|
def increment_view_count(self, page_id):
|
|
"""Increment view count for a page"""
|
|
if page_id in self.link_pages_db:
|
|
self.link_pages_db[page_id]['view_count'] += 1
|
|
self._save_link_pages() # Persist to file
|
|
|
|
def get_page(self, page_id):
|
|
"""Get page data"""
|
|
# Reload data from file to ensure we have the latest data
|
|
self.link_pages_db = self._load_link_pages()
|
|
return self.link_pages_db.get(page_id)
|
|
|
|
def page_exists(self, page_id):
|
|
"""Check if page exists"""
|
|
# Reload data from file to ensure we have the latest data
|
|
self.link_pages_db = self._load_link_pages()
|
|
return page_id in self.link_pages_db
|
|
|
|
# URL Shortener management methods
|
|
def create_standalone_short_url(self, url, title="", custom_code=None):
|
|
"""Create a standalone short URL (not tied to a link page)"""
|
|
return self.url_shortener.create_short_url(url, custom_code, title)
|
|
|
|
def get_short_url_stats(self, short_code):
|
|
"""Get statistics for a short URL"""
|
|
return self.url_shortener.get_url_stats(short_code)
|
|
|
|
def list_all_short_urls(self):
|
|
"""List all short URLs in the system"""
|
|
return self.url_shortener.list_urls()
|
|
|
|
def resolve_short_url(self, short_code):
|
|
"""Resolve a short URL to its original URL"""
|
|
# Reload URLShortener data to ensure we have the latest URLs
|
|
self.url_shortener = URLShortener()
|
|
return self.url_shortener.get_original_url(short_code)
|