diff --git a/docs/md_v2/marketplace/admin/admin.js b/docs/md_v2/marketplace/admin/admin.js index adb31d0b..7d31a826 100644 --- a/docs/md_v2/marketplace/admin/admin.js +++ b/docs/md_v2/marketplace/admin/admin.js @@ -1,10 +1,38 @@ // Admin Dashboard - Smart & Powerful const { API_BASE, API_ORIGIN } = (() => { - const { hostname, port } = window.location; - if ((hostname === 'localhost' || hostname === '127.0.0.1') && port === '8000') { - const origin = 'http://127.0.0.1:8100'; - return { API_BASE: `${origin}/api`, API_ORIGIN: origin }; + const cleanOrigin = (value) => value ? value.replace(/\/$/, '') : ''; + const params = new URLSearchParams(window.location.search); + const overrideParam = cleanOrigin(params.get('api_origin')); + + let storedOverride = ''; + try { + storedOverride = cleanOrigin(localStorage.getItem('marketplace_api_origin')); + } catch (error) { + storedOverride = ''; } + + let origin = overrideParam || storedOverride; + + if (overrideParam && overrideParam !== storedOverride) { + try { + localStorage.setItem('marketplace_api_origin', overrideParam); + } catch (error) { + // ignore storage errors (private mode, etc.) + } + } + + const { protocol, hostname, port } = window.location; + const isLocalHost = ['localhost', '127.0.0.1', '0.0.0.0'].includes(hostname); + + if (!origin && isLocalHost && port !== '8100') { + origin = `${protocol}//127.0.0.1:8100`; + } + + if (origin) { + const normalized = cleanOrigin(origin); + return { API_BASE: `${normalized}/api`, API_ORIGIN: normalized }; + } + return { API_BASE: '/api', API_ORIGIN: '' }; })(); @@ -185,7 +213,9 @@ class AdminDashboard { } async loadStats() { - const stats = await this.apiCall('/admin/stats'); + const stats = await this.apiCall(`/admin/stats?_=${Date.now()}`, { + cache: 'no-store' + }); document.getElementById('stat-apps').textContent = stats.apps.total; document.getElementById('stat-featured').textContent = stats.apps.featured; @@ -196,17 +226,24 @@ class AdminDashboard { } async loadApps() { - this.data.apps = await this.apiCall('/apps?limit=100'); + this.data.apps = await this.apiCall(`/apps?limit=100&_=${Date.now()}`, { + cache: 'no-store' + }); this.renderAppsTable(this.data.apps); } async loadArticles() { - this.data.articles = await this.apiCall('/articles?limit=100'); + this.data.articles = await this.apiCall(`/articles?limit=100&_=${Date.now()}`, { + cache: 'no-store' + }); this.renderArticlesTable(this.data.articles); } async loadCategories() { - this.data.categories = await this.apiCall('/categories'); + const cacheBuster = Date.now(); + this.data.categories = await this.apiCall(`/categories?_=${cacheBuster}`, { + cache: 'no-store' + }); this.renderCategoriesTable(this.data.categories); } @@ -664,8 +701,10 @@ class AdminDashboard { data.description = document.getElementById('form-description').value; data.category = document.getElementById('form-category').value; data.type = document.getElementById('form-type').value; - data.rating = parseFloat(document.getElementById('form-rating').value); - data.downloads = parseInt(document.getElementById('form-downloads').value); + const rating = parseFloat(document.getElementById('form-rating').value); + const downloads = parseInt(document.getElementById('form-downloads').value, 10); + data.rating = Number.isFinite(rating) ? rating : 0; + data.downloads = Number.isFinite(downloads) ? downloads : 0; data.image = document.getElementById('form-image').value; data.website_url = document.getElementById('form-website').value; data.github_url = document.getElementById('form-github').value; @@ -686,7 +725,8 @@ class AdminDashboard { data.slug = this.generateSlug(data.name); data.icon = document.getElementById('form-icon').value; data.description = document.getElementById('form-description').value; - data.order_index = parseInt(document.getElementById('form-order').value); + const orderIndex = parseInt(document.getElementById('form-order').value, 10); + data.order_index = Number.isFinite(orderIndex) ? orderIndex : 0; } else if (type === 'sponsors') { data.company_name = document.getElementById('form-name').value; data.logo_url = document.getElementById('form-logo-url').value; diff --git a/docs/md_v2/marketplace/admin/index.html b/docs/md_v2/marketplace/admin/index.html index 3691c1eb..a9b649f2 100644 --- a/docs/md_v2/marketplace/admin/index.html +++ b/docs/md_v2/marketplace/admin/index.html @@ -210,6 +210,6 @@ - + \ No newline at end of file diff --git a/docs/md_v2/marketplace/backend/server.py b/docs/md_v2/marketplace/backend/server.py index 77540020..0f177386 100644 --- a/docs/md_v2/marketplace/backend/server.py +++ b/docs/md_v2/marketplace/backend/server.py @@ -7,6 +7,7 @@ from typing import Optional, Dict, Any import json import hashlib import secrets +import re from pathlib import Path from database import DatabaseManager from datetime import datetime, timedelta @@ -58,6 +59,29 @@ def json_response(data, cache_time=3600): } ) + +def to_int(value, default=0): + """Coerce incoming values to integers, falling back to default.""" + if value is None: + return default + if isinstance(value, bool): + return int(value) + if isinstance(value, (int, float)): + return int(value) + + if isinstance(value, str): + stripped = value.strip() + if not stripped: + return default + + match = re.match(r"^-?\d+", stripped) + if match: + try: + return int(match.group()) + except ValueError: + return default + return default + # ============= PUBLIC ENDPOINTS ============= @app.get("/api/apps") @@ -141,6 +165,8 @@ async def get_article(slug: str): async def get_categories(): """Get all categories ordered by index""" categories = db.get_all('categories', limit=50) + for category in categories: + category['order_index'] = to_int(category.get('order_index'), 0) categories.sort(key=lambda x: x.get('order_index', 0)) return json_response(categories, cache_time=7200) @@ -360,6 +386,9 @@ async def delete_article(article_id: int): async def create_category(category_data: Dict[str, Any]): """Create new category""" try: + category_data = dict(category_data) + category_data['order_index'] = to_int(category_data.get('order_index'), 0) + cursor = db.conn.cursor() columns = ', '.join(category_data.keys()) placeholders = ', '.join(['?' for _ in category_data]) @@ -374,6 +403,10 @@ async def create_category(category_data: Dict[str, Any]): async def update_category(cat_id: int, category_data: Dict[str, Any]): """Update category""" try: + category_data = dict(category_data) + if 'order_index' in category_data: + category_data['order_index'] = to_int(category_data.get('order_index'), 0) + set_clause = ', '.join([f"{k} = ?" for k in category_data.keys()]) cursor = db.conn.cursor() cursor.execute(f"UPDATE categories SET {set_clause} WHERE id = ?", @@ -383,6 +416,18 @@ async def update_category(cat_id: int, category_data: Dict[str, Any]): except Exception as e: raise HTTPException(status_code=400, detail=str(e)) + +@app.delete("/api/admin/categories/{cat_id}", dependencies=[Depends(verify_token)]) +async def delete_category(cat_id: int): + """Delete category""" + try: + cursor = db.conn.cursor() + cursor.execute("DELETE FROM categories WHERE id = ?", (cat_id,)) + db.conn.commit() + return {"message": "Category deleted"} + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + # Sponsors CRUD @app.post("/api/admin/sponsors", dependencies=[Depends(verify_token)]) async def create_sponsor(sponsor_data: Dict[str, Any]): @@ -411,6 +456,18 @@ async def update_sponsor(sponsor_id: int, sponsor_data: Dict[str, Any]): except Exception as e: raise HTTPException(status_code=400, detail=str(e)) + +@app.delete("/api/admin/sponsors/{sponsor_id}", dependencies=[Depends(verify_token)]) +async def delete_sponsor(sponsor_id: int): + """Delete sponsor""" + try: + cursor = db.conn.cursor() + cursor.execute("DELETE FROM sponsors WHERE id = ?", (sponsor_id,)) + db.conn.commit() + return {"message": "Sponsor deleted"} + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + @app.get("/") async def root(): """API info"""