Commit ceafa183 authored by Your Name's avatar Your Name

Ties system and bug fixes

parent f4c1b651
# AISBF Database Migration Guide
## Overview
AISBF uses two separate SQLite databases with distinct purposes:
1. **`aisbf.db`** - Configuration and persistent data
2. **`cache.db`** - Temporary caching only
## Database Separation
### aisbf.db (Configuration Database)
This database contains all configuration and persistent data:
**User Management:**
- `users` - User accounts and authentication
- `user_api_tokens` - API tokens for users
- `user_providers` - User-specific provider configurations
- `user_rotations` - User-specific rotation configurations
- `user_autoselects` - User-specific autoselect configurations
- `user_prompts` - User-specific prompt overrides
- `user_auth_files` - User authentication file metadata
- `user_oauth2_credentials` - OAuth2 credentials per user/provider
**Billing & Subscriptions:**
- `account_tiers` - Subscription tier definitions
- `payment_methods` - User payment methods
- `user_subscriptions` - Active subscriptions
- `payment_transactions` - Payment history
**Analytics & Tracking:**
- `context_dimensions` - Context usage tracking
- `token_usage` - Token usage for rate limiting
- `user_token_usage` - User-specific token usage
- `model_embeddings` - Cached model embeddings
### cache.db (Cache Database)
This database contains ONLY temporary caching data:
- `cache` - General purpose cache
- `response_cache` - AI response caching
## Migration Issue
In some installations, configuration tables (especially `users`) were incorrectly created in `cache.db` instead of `aisbf.db`. This causes issues because:
1. Configuration data should persist across cache clears
2. The application expects configuration in `aisbf.db`
3. Cache database should be safe to delete without losing data
## Migration Process
### Step 1: Check Current State
First, verify which database contains your data:
```bash
# Check tables in cache.db
sqlite3 ~/.aisbf/cache.db ".tables"
# Check tables in aisbf.db
sqlite3 ~/.aisbf/aisbf.db ".tables"
```
If you see configuration tables (like `users`, `user_providers`, etc.) in `cache.db`, you need to migrate.
### Step 2: Dry Run
Test the migration without making changes:
```bash
python migrate_cache_to_aisbf.py --dry-run
```
This will show you:
- Which tables will be migrated
- How many rows will be copied
- Any potential issues
### Step 3: Perform Migration
Run the actual migration:
```bash
python migrate_cache_to_aisbf.py
```
The script will:
1. Create backups of both databases
2. Copy configuration tables from `cache.db` to `aisbf.db`
3. Preserve all existing data
4. Show a summary of migrated data
**Backup files are created automatically:**
- `~/.aisbf/cache_backup_YYYYMMDD_HHMMSS.db`
- `~/.aisbf/aisbf_backup_YYYYMMDD_HHMMSS.db`
### Step 4: Verify Migration
After migration, verify the data:
```bash
# Check users table in aisbf.db
sqlite3 ~/.aisbf/aisbf.db "SELECT COUNT(*) FROM users;"
# Check your user exists
sqlite3 ~/.aisbf/aisbf.db "SELECT username, role FROM users;"
```
### Step 5: Test Application
Start AISBF and verify:
1. You can log in with your existing credentials
2. All providers and rotations are available
3. User-specific configurations are preserved
```bash
# Start AISBF
aisbf
# Or if running from source
python main.py
```
### Step 6: Cleanup (Optional)
After confirming everything works, clean up `cache.db`:
```bash
python migrate_cache_to_aisbf.py --cleanup
```
This removes configuration tables from `cache.db`, leaving only cache tables.
## Advanced Options
### Force Overwrite
If destination tables already have data and you want to overwrite:
```bash
python migrate_cache_to_aisbf.py --force
```
### Custom Database Paths
If your databases are in non-standard locations:
```bash
python migrate_cache_to_aisbf.py \
--cache-db /path/to/cache.db \
--aisbf-db /path/to/aisbf.db
```
## Troubleshooting
### Issue: "Table already has rows in destination"
**Solution:** Use `--force` to overwrite, or manually inspect both databases to determine which has the correct data.
### Issue: Migration fails with "database is locked"
**Solution:** Stop AISBF before running migration:
```bash
# Stop AISBF
aisbf stop
# Run migration
python migrate_cache_to_aisbf.py
# Start AISBF
aisbf
```
### Issue: Lost data after migration
**Solution:** Restore from backup:
```bash
# Find your backup
ls -lt ~/.aisbf/*_backup_*.db
# Restore cache.db
cp ~/.aisbf/cache_backup_YYYYMMDD_HHMMSS.db ~/.aisbf/cache.db
# Restore aisbf.db
cp ~/.aisbf/aisbf_backup_YYYYMMDD_HHMMSS.db ~/.aisbf/aisbf.db
```
## Prevention
The code has been updated to ensure proper database separation:
1. **`database.py`** - All methods use `aisbf.db` for configuration
2. **`cache.py`** - All methods use `cache.db` for caching only
3. **Initialization** - Databases are created with correct table separation
After upgrading to the fixed version, new installations will automatically use the correct database structure.
## For Developers
### Database Initialization
```python
from aisbf.database import initialize_database, get_database
# Initialize configuration database (aisbf.db)
initialize_database()
# Get database manager
db = get_database()
# All operations use aisbf.db
user = db.authenticate_user(username, password_hash)
```
### Cache Operations
```python
from aisbf.cache import get_cache_manager
# Initialize cache (cache.db)
cache = get_cache_manager()
# All operations use cache.db
cache.set('key', 'value', ttl=600)
value = cache.get('key')
```
### Adding New Tables
**Configuration tables** (add to `database.py`):
```python
cursor.execute('''
CREATE TABLE IF NOT EXISTS my_config_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
...
)
''')
```
**Cache tables** (add to `cache.py`):
```python
cursor.execute('''
CREATE TABLE IF NOT EXISTS my_cache_table (
key TEXT PRIMARY KEY,
value TEXT,
ttl REAL
)
''')
```
## Summary
- **aisbf.db** = Configuration & persistent data (users, providers, etc.)
- **cache.db** = Temporary caching only (cache, response_cache)
- **Migration script** = Moves misplaced tables from cache.db to aisbf.db
- **Backups** = Created automatically before any changes
- **Safe** = Can be run multiple times, dry-run available
For questions or issues, refer to the main DOCUMENTATION.md or open an issue on GitHub.
...@@ -48,7 +48,7 @@ class KiloOAuth2: ...@@ -48,7 +48,7 @@ class KiloOAuth2:
credentials_file: Path to credentials JSON file (default: ~/.kilo_credentials.json) credentials_file: Path to credentials JSON file (default: ~/.kilo_credentials.json)
api_base: Base URL for Kilo API (default: https://api.kilo.ai) api_base: Base URL for Kilo API (default: https://api.kilo.ai)
""" """
self.credentials_file = credentials_file or os.path.expanduser("~/.kilo_credentials.json") self.credentials_file = os.path.expanduser(credentials_file) if credentials_file else os.path.expanduser("~/.kilo_credentials.json")
self.api_base = api_base or os.environ.get("KILO_API_URL", "https://api.kilo.ai") self.api_base = api_base or os.environ.get("KILO_API_URL", "https://api.kilo.ai")
self.credentials = None self.credentials = None
self._load_credentials() self._load_credentials()
...@@ -73,7 +73,9 @@ class KiloOAuth2: ...@@ -73,7 +73,9 @@ class KiloOAuth2:
""" """
try: try:
# Ensure directory exists # Ensure directory exists
os.makedirs(os.path.dirname(self.credentials_file), exist_ok=True) cred_dir = os.path.dirname(self.credentials_file)
if cred_dir: # Only create if there's a directory component
os.makedirs(cred_dir, exist_ok=True)
# Write credentials # Write credentials
with open(self.credentials_file, 'w') as f: with open(self.credentials_file, 'w') as f:
......
...@@ -219,6 +219,24 @@ class SignupConfig(BaseModel): ...@@ -219,6 +219,24 @@ class SignupConfig(BaseModel):
require_email_verification: bool = True require_email_verification: bool = True
verification_token_expiry_hours: int = 24 verification_token_expiry_hours: int = 24
class PaymentGatewayConfig(BaseModel):
"""Configuration for payment gateways"""
enabled: bool = False
public_key: Optional[str] = None
secret_key: Optional[str] = None
webhook_secret: Optional[str] = None
api_url: Optional[str] = None
wallet_address: Optional[str] = None
minimum_amount: Optional[float] = None
additional_config: Optional[Dict] = None
class CurrencyConfig(BaseModel):
"""Global currency configuration"""
code: str = "USD"
symbol: str = "$"
decimal_places: int = 2
position: str = "left"
class SMTPConfig(BaseModel): class SMTPConfig(BaseModel):
"""Configuration for SMTP email sending""" """Configuration for SMTP email sending"""
host: str = "localhost" host: str = "localhost"
...@@ -248,6 +266,8 @@ class AISBFConfig(BaseModel): ...@@ -248,6 +266,8 @@ class AISBFConfig(BaseModel):
adaptive_rate_limiting: Optional[AdaptiveRateLimitingConfig] = None adaptive_rate_limiting: Optional[AdaptiveRateLimitingConfig] = None
signup: Optional[SignupConfig] = None signup: Optional[SignupConfig] = None
smtp: Optional[SMTPConfig] = None smtp: Optional[SMTPConfig] = None
currency: Optional[CurrencyConfig] = None
payment_gateways: Optional[Dict[str, PaymentGatewayConfig]] = None
class AppConfig(BaseModel): class AppConfig(BaseModel):
...@@ -728,6 +748,14 @@ class Config: ...@@ -728,6 +748,14 @@ class Config:
smtp_data = data.get('smtp') smtp_data = data.get('smtp')
if smtp_data: if smtp_data:
data['smtp'] = SMTPConfig(**smtp_data) data['smtp'] = SMTPConfig(**smtp_data)
# Parse currency separately if present
currency_data = data.get('currency')
if currency_data:
data['currency'] = CurrencyConfig(**currency_data)
# Parse payment gateways separately if present
payment_gateways_data = data.get('payment_gateways')
if payment_gateways_data:
data['payment_gateways'] = {k: PaymentGatewayConfig(**v) for k, v in payment_gateways_data.items()}
self.aisbf = AISBFConfig(**data) self.aisbf = AISBFConfig(**data)
self._loaded_files['aisbf'] = str(aisbf_path.absolute()) self._loaded_files['aisbf'] = str(aisbf_path.absolute())
logger.info(f"Loaded AISBF config: classify_nsfw={self.aisbf.classify_nsfw}, classify_privacy={self.aisbf.classify_privacy}") logger.info(f"Loaded AISBF config: classify_nsfw={self.aisbf.classify_nsfw}, classify_privacy={self.aisbf.classify_privacy}")
......
This diff is collapsed.
...@@ -88,3 +88,68 @@ class ErrorTracking(BaseModel): ...@@ -88,3 +88,68 @@ class ErrorTracking(BaseModel):
failures: int failures: int
last_failure: Optional[int] last_failure: Optional[int]
disabled_until: Optional[int] disabled_until: Optional[int]
class AccountTier(BaseModel):
id: Optional[int] = None
name: str
description: Optional[str] = None
price_monthly: float = 0.0
price_yearly: float = 0.0
is_default: bool = False
is_active: bool = True
# Limits
max_requests_per_day: int = -1
max_requests_per_month: int = -1
max_providers: int = -1
max_rotations: int = -1
max_autoselections: int = -1
max_rotation_models: int = -1
max_autoselection_models: int = -1
created_at: Optional[int] = None
updated_at: Optional[int] = None
class UserSubscription(BaseModel):
id: Optional[int] = None
user_id: int
tier_id: int
status: str = "active" # active, canceled, expired, suspended
start_date: int
end_date: Optional[int] = None
next_billing_date: Optional[int] = None
trial_end_date: Optional[int] = None
payment_method_id: Optional[int] = None
auto_renew: bool = True
created_at: Optional[int] = None
updated_at: Optional[int] = None
class PaymentMethod(BaseModel):
id: Optional[int] = None
user_id: int
type: str # paypal, stripe, bitcoin, eth, usdt, usdc
identifier: str
is_default: bool = False
is_active: bool = True
metadata: Optional[Dict] = None
created_at: Optional[int] = None
updated_at: Optional[int] = None
class PaymentTransaction(BaseModel):
id: Optional[int] = None
user_id: int
tier_id: Optional[int] = None
subscription_id: Optional[int] = None
payment_method_id: Optional[int] = None
amount: float
currency: str = "USD"
status: str # pending, completed, failed, refunded
transaction_type: str # subscription, one_time, renewal
external_transaction_id: Optional[str] = None
metadata: Optional[Dict] = None
created_at: Optional[int] = None
completed_at: Optional[int] = None
...@@ -146,5 +146,49 @@ ...@@ -146,5 +146,49 @@
"client_secret": "", "client_secret": "",
"scopes": ["user:email", "read:user"] "scopes": ["user:email", "read:user"]
} }
},
"billing": {
"currency": "USD",
"currency_symbol": "$",
"currency_decimals": 2,
"payment_methods": {
"paypal": {
"enabled": false,
"client_id": "",
"client_secret": "",
"mode": "sandbox"
},
"stripe": {
"enabled": false,
"publishable_key": "",
"secret_key": "",
"webhook_secret": "",
"mode": "test"
},
"bitcoin": {
"enabled": false,
"wallet_address": "",
"confirmations_required": 3
},
"eth": {
"enabled": false,
"wallet_address": "",
"chain_id": 1,
"confirmations_required": 12
},
"usdt": {
"enabled": false,
"wallet_address": "",
"chain": "ethereum",
"confirmations_required": 12
},
"usdc": {
"enabled": false,
"wallet_address": "",
"chain": "ethereum",
"confirmations_required": 12
}
}
} }
} }
#!/usr/bin/env python3
"""
Database diagnostic script for AISBF
This script checks which database contains which tables and helps identify
database separation issues.
Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
"""
import sqlite3
import sys
from pathlib import Path
from typing import List, Tuple
def get_tables(db_path: Path) -> List[str]:
"""Get list of tables in a database"""
if not db_path.exists():
return []
try:
with sqlite3.connect(str(db_path)) as conn:
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")
return [row[0] for row in cursor.fetchall()]
except Exception as e:
print(f"Error reading {db_path}: {e}")
return []
def get_table_count(db_path: Path, table_name: str) -> int:
"""Get row count for a table"""
if not db_path.exists():
return 0
try:
with sqlite3.connect(str(db_path)) as conn:
cursor = conn.cursor()
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
return cursor.fetchone()[0]
except Exception as e:
return 0
def main():
print("=" * 70)
print("AISBF Database Diagnostic Tool")
print("=" * 70)
print()
# Database paths
aisbf_dir = Path.home() / '.aisbf'
cache_db = aisbf_dir / 'cache.db'
aisbf_db = aisbf_dir / 'aisbf.db'
response_cache_db = aisbf_dir / 'response_cache.db'
print(f"Checking databases in: {aisbf_dir}")
print()
# Configuration tables (should be in aisbf.db)
config_tables = [
'users', 'user_providers', 'user_rotations', 'user_autoselects',
'user_prompts', 'user_api_tokens', 'user_token_usage',
'user_auth_files', 'user_oauth2_credentials',
'account_tiers', 'payment_methods', 'user_subscriptions',
'payment_transactions', 'context_dimensions', 'token_usage',
'model_embeddings'
]
# Cache tables (should be in cache.db)
cache_tables = ['cache', 'response_cache']
# Check cache.db
print("📁 cache.db")
print("-" * 70)
if cache_db.exists():
cache_db_tables = get_tables(cache_db)
print(f" Tables found: {len(cache_db_tables)}")
# Check for misplaced configuration tables
misplaced = [t for t in cache_db_tables if t in config_tables]
if misplaced:
print(f" ⚠️ WARNING: Configuration tables found in cache.db:")
for table in misplaced:
count = get_table_count(cache_db, table)
print(f" - {table} ({count} rows)")
# Check for correct cache tables
correct = [t for t in cache_db_tables if t in cache_tables]
if correct:
print(f" ✅ Cache tables (correct):")
for table in correct:
count = get_table_count(cache_db, table)
print(f" - {table} ({count} rows)")
# Unknown tables
unknown = [t for t in cache_db_tables if t not in config_tables and t not in cache_tables]
if unknown:
print(f" ❓ Unknown tables:")
for table in unknown:
count = get_table_count(cache_db, table)
print(f" - {table} ({count} rows)")
else:
print(" ❌ Database does not exist")
print()
# Check aisbf.db
print("📁 aisbf.db")
print("-" * 70)
if aisbf_db.exists():
aisbf_db_tables = get_tables(aisbf_db)
print(f" Tables found: {len(aisbf_db_tables)}")
# Check for correct configuration tables
correct = [t for t in aisbf_db_tables if t in config_tables]
if correct:
print(f" ✅ Configuration tables (correct):")
for table in correct:
count = get_table_count(aisbf_db, table)
print(f" - {table} ({count} rows)")
# Check for misplaced cache tables
misplaced = [t for t in aisbf_db_tables if t in cache_tables]
if misplaced:
print(f" ⚠️ WARNING: Cache tables found in aisbf.db:")
for table in misplaced:
count = get_table_count(aisbf_db, table)
print(f" - {table} ({count} rows)")
# Unknown tables
unknown = [t for t in aisbf_db_tables if t not in config_tables and t not in cache_tables]
if unknown:
print(f" ❓ Unknown tables:")
for table in unknown:
count = get_table_count(aisbf_db, table)
print(f" - {table} ({count} rows)")
else:
print(" ❌ Database does not exist")
print()
# Check response_cache.db
print("📁 response_cache.db")
print("-" * 70)
if response_cache_db.exists():
response_cache_db_tables = get_tables(response_cache_db)
print(f" Tables found: {len(response_cache_db_tables)}")
for table in response_cache_db_tables:
count = get_table_count(response_cache_db, table)
print(f" - {table} ({count} rows)")
else:
print(" ❌ Database does not exist")
print()
# Summary and recommendations
print("=" * 70)
print("Summary")
print("=" * 70)
issues_found = False
# Check for configuration tables in cache.db
if cache_db.exists():
cache_db_tables = get_tables(cache_db)
misplaced_in_cache = [t for t in cache_db_tables if t in config_tables]
if misplaced_in_cache:
issues_found = True
print("❌ ISSUE: Configuration tables found in cache.db")
print(f" Tables: {', '.join(misplaced_in_cache)}")
print()
print(" SOLUTION: Run the migration script:")
print(" python migrate_cache_to_aisbf.py")
print()
# Check for cache tables in aisbf.db
if aisbf_db.exists():
aisbf_db_tables = get_tables(aisbf_db)
misplaced_in_aisbf = [t for t in aisbf_db_tables if t in cache_tables]
if misplaced_in_aisbf:
issues_found = True
print("⚠️ WARNING: Cache tables found in aisbf.db")
print(f" Tables: {', '.join(misplaced_in_aisbf)}")
print(" This is unusual but not critical.")
print()
if not issues_found:
print("✅ All tables are in the correct databases!")
print()
print("Database separation is correct:")
print(" - aisbf.db contains configuration tables")
print(" - cache.db contains cache tables only")
print()
print("=" * 70)
return 0 if not issues_found else 1
if __name__ == '__main__':
sys.exit(main())
This diff is collapsed.
This diff is collapsed.
...@@ -49,7 +49,7 @@ class InstallCommand(_install): ...@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup( setup(
name="aisbf", name="aisbf",
version="0.99.20", version="0.99.21",
author="AISBF Contributors", author="AISBF Contributors",
author_email="stefy@nexlab.net", author_email="stefy@nexlab.net",
description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations", description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations",
...@@ -172,6 +172,11 @@ setup( ...@@ -172,6 +172,11 @@ setup(
'templates/dashboard/signup.html', 'templates/dashboard/signup.html',
'templates/dashboard/profile.html', 'templates/dashboard/profile.html',
'templates/dashboard/change_password.html', 'templates/dashboard/change_password.html',
'templates/dashboard/admin_tiers.html',
'templates/dashboard/admin_tier_form.html',
'templates/dashboard/pricing.html',
'templates/dashboard/subscription.html',
'templates/dashboard/billing.html',
]), ]),
# Install static files (extension and favicon) # Install static files (extension and favicon)
('share/aisbf/static', [ ('share/aisbf/static', [
......
...@@ -63,6 +63,47 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -63,6 +63,47 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
.nav .account-dropdown a:first-child { border-radius: 8px 8px 0 0; } .nav .account-dropdown a:first-child { border-radius: 8px 8px 0 0; }
.nav .account-dropdown a:last-child { border-radius: 0 0 8px 8px; } .nav .account-dropdown a:last-child { border-radius: 0 0 8px 8px; }
.nav .account-dropdown a:hover { background: #0f3460; color: #e0e0e0; } .nav .account-dropdown a:hover { background: #0f3460; color: #e0e0e0; }
/* Rainbow Upgrade Button */
.upgrade-button {
font-weight: 700 !important;
padding: 8px 16px !important;
border-radius: 6px !important;
background: linear-gradient(120deg, #ff0080, #ff8c00, #40e0d0, #9370db, #ff0080) !important;
background-size: 400% 400% !important;
animation: rainbow-gradient 3s ease infinite !important;
color: white !important;
text-shadow: 0 1px 2px rgba(0,0,0,0.3) !important;
box-shadow: 0 0 12px rgba(255, 0, 128, 0.5) !important;
margin-left: 4px !important;
transition: transform 0.2s ease, box-shadow 0.2s ease !important;
}
.upgrade-button:hover {
transform: scale(1.05) !important;
box-shadow: 0 0 20px rgba(255, 0, 128, 0.7) !important;
}
@keyframes rainbow-gradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.rainbow-text {
background: linear-gradient(90deg, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #4b0082, #9400d3);
background-size: 400% 400%;
animation: rainbow-text 2s linear infinite;
-webkit-background-clip: text;
background-clip: text;
color: transparent !important;
font-weight: 800 !important;
}
@keyframes rainbow-text {
0% { background-position: 0% center; }
100% { background-position: 400% center; }
}
</style> </style>
<script> <script>
/* /*
...@@ -90,7 +131,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -90,7 +131,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
document.addEventListener('click', function(event) { document.addEventListener('click', function(event) {
const accountMenu = document.querySelector('.account-menu'); const accountMenu = document.querySelector('.account-menu');
const dropdown = document.getElementById('account-dropdown'); const dropdown = document.getElementById('account-dropdown');
if (!accountMenu.contains(event.target)) { if (accountMenu && dropdown && !accountMenu.contains(event.target)) {
dropdown.classList.remove('active'); dropdown.classList.remove('active');
} }
}); });
...@@ -423,6 +464,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -423,6 +464,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<h1>AISBF Dashboard</h1> <h1>AISBF Dashboard</h1>
{% if request.session.logged_in %} {% if request.session.logged_in %}
<div class="header-actions"> <div class="header-actions">
<a href="{{ url_for(request, '/dashboard/docs') }}" class="btn btn-secondary">Docs</a>
<a href="{{ url_for(request, '/dashboard/about') }}" class="btn btn-secondary">About</a>
<a href="{{ url_for(request, '/dashboard/license') }}" class="btn btn-secondary">License</a> <a href="{{ url_for(request, '/dashboard/license') }}" class="btn btn-secondary">License</a>
{% if request.session.role == 'admin' %} {% if request.session.role == 'admin' %}
<button onclick="restartServer()" class="btn btn-warning">Restart Server</button> <button onclick="restartServer()" class="btn btn-warning">Restart Server</button>
...@@ -433,6 +476,22 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -433,6 +476,22 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div> </div>
</div> </div>
{% if request.session.logged_in and show_upgrade_banner %}
<div class="upgrade-banner">
<div class="container">
<div class="d-flex align-items-center justify-content-between py-2">
<div>
<i class="fas fa-rocket me-2"></i>
<span>Want higher limits? <strong>Upgrade your plan</strong> for more requests and features!</span>
</div>
<a href="{{ url_for(request, '/dashboard/pricing') }}" class="btn btn-sm btn-light">
View Pricing Plans <i class="fas fa-arrow-right ms-1"></i>
</a>
</div>
</div>
</div>
{% endif %}
{% if request.session.logged_in %} {% if request.session.logged_in %}
<div class="container"> <div class="container">
<div class="nav"> <div class="nav">
...@@ -445,9 +504,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -445,9 +504,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% if request.session.role == 'admin' %} {% if request.session.role == 'admin' %}
<a href="{{ url_for(request, '/dashboard/users') }}" {% if '/users' in request.path %}class="active"{% endif %}>Users</a> <a href="{{ url_for(request, '/dashboard/users') }}" {% if '/users' in request.path %}class="active"{% endif %}>Users</a>
<a href="{{ url_for(request, '/dashboard/settings') }}" {% if '/settings' in request.path %}class="active"{% endif %}>Settings</a> <a href="{{ url_for(request, '/dashboard/settings') }}" {% if '/settings' in request.path %}class="active"{% endif %}>Settings</a>
<a href="{{ url_for(request, '/dashboard/admin/tiers') }}" {% if '/admin/tiers' in request.path %}class="active"{% endif %}>Tiers</a>
{% endif %}
{% if show_upgrade_button %}
<a href="{{ url_for(request, '/dashboard/pricing') }}" class="upgrade-button rainbow-text">✨ Upgrade! ✨</a>
{% endif %} {% endif %}
<a href="{{ url_for(request, '/dashboard/docs') }}" {% if '/docs' in request.path %}class="active"{% endif %}>Docs</a>
<a href="{{ url_for(request, '/dashboard/about') }}" {% if '/about' in request.path %}class="active"{% endif %}>About</a>
{% if request.session.user_id %} {% if request.session.user_id %}
<div class="account-menu"> <div class="account-menu">
<div class="account-trigger" onclick="toggleAccountMenu()"> <div class="account-trigger" onclick="toggleAccountMenu()">
...@@ -457,8 +518,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -457,8 +518,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="account-dropdown" id="account-dropdown"> <div class="account-dropdown" id="account-dropdown">
{% if request.session.user_id %} {% if request.session.user_id %}
{% if request.session.user_id %} {% if request.session.user_id %}
<a href="{{ url_for(request, '/dashboard/profile') }}">Edit Profile</a> <a href="{{ url_for(request, '/dashboard/profile') }}">Edit Profile</a>
<a href="{{ url_for(request, '/dashboard/change-password') }}">Change Password</a> <a href="{{ url_for(request, '/dashboard/subscription') }}">Subscription</a>
<a href="{{ url_for(request, '/dashboard/billing') }}">Billing</a>
<a href="{{ url_for(request, '/dashboard/change-password') }}">Change Password</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
...@@ -473,5 +536,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -473,5 +536,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
</div> </div>
{% block extra_js %}{% endblock %}
</body> </body>
</html> </html>
{% extends "base.html" %}
{% block title %}{{ 'Edit' if tier else 'Create' }} Tier{% endblock %}
{% block content %}
<div style="max-width: 800px; margin: 0 auto;">
<h2 style="margin-bottom: 30px;">
<i class="fas fa-layer-group" style="color: #4a9eff;"></i>
{{ 'Edit Tier' if tier else 'Create New Tier' }}
</h2>
<form method="POST" action="{{ url_for(request, '/dashboard/admin/tiers/save') }}">
<input type="hidden" name="tier_id" value="{{ tier.id if tier else '' }}">
<div style="background: #16213e; border: 2px solid #4a9eff; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<div style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0; font-size: 16px;">Tier Name</label>
<input type="text" name="name" style="width: 100%; padding: 12px; border: 1px solid #0f3460; border-radius: 5px; background: #1a1a2e; color: #e0e0e0; font-size: 16px;" required placeholder="e.g. Professional Tier" value="{{ tier.name if tier else '' }}">
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0; font-size: 16px;">Description</label>
<textarea name="description" style="width: 100%; padding: 12px; border: 1px solid #0f3460; border-radius: 5px; background: #1a1a2e; color: #e0e0e0; font-size: 16px; min-height: 80px;" placeholder="Tier description">{{ tier.description if tier else '' }}</textarea>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; margin-bottom: 20px;">
<div style="background: #1a1a2e; border: 1px solid #0f3460; border-radius: 8px; padding: 15px;">
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #4a9eff; font-size: 14px;">
<i class="fas fa-calendar-day me-2"></i> Monthly Price
</label>
<div style="display: flex; align-items: center;">
<span style="background: #0f3460; color: #e0e0e0; padding: 12px; border-radius: 5px 0 0 5px; border: 1px solid #0f3460;">$</span>
<input type="number" name="price_monthly" style="flex: 1; padding: 12px; border: 1px solid #0f3460; border-left: none; border-radius: 0 5px 5px 0; background: #16213e; color: #e0e0e0; font-size: 16px;" step="0.01" min="0" value="{{ tier.price_monthly if tier else '0.00' }}">
</div>
</div>
<div style="background: #1a1a2e; border: 1px solid #0f3460; border-radius: 8px; padding: 15px;">
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #4ade80; font-size: 14px;">
<i class="fas fa-calendar-alt me-2"></i> Yearly Price
</label>
<div style="display: flex; align-items: center;">
<span style="background: #0f3460; color: #e0e0e0; padding: 12px; border-radius: 5px 0 0 5px; border: 1px solid #0f3460;">$</span>
<input type="number" name="price_yearly" style="flex: 1; padding: 12px; border: 1px solid #0f3460; border-left: none; border-radius: 0 5px 5px 0; background: #16213e; color: #e0e0e0; font-size: 16px;" step="0.01" min="0" value="{{ tier.price_yearly if tier else '0.00' }}">
</div>
</div>
</div>
<div style="background: #1a1a2e; border: 1px solid #0f3460; border-radius: 8px; margin-bottom: 20px; overflow: hidden;">
<div style="padding: 15px; border-bottom: 1px solid #0f3460;">
<h6 style="margin: 0; color: #e0e0e0; font-weight: 500;"><i class="fas fa-sliders-h me-2"></i> Usage Limits</h6>
<p style="margin: 5px 0 0 0; color: #a0a0a0; font-size: 12px;">Use <code style="background: #0f3460; padding: 2px 4px; border-radius: 3px;">-1</code> for unlimited, <code style="background: #0f3460; padding: 2px 4px; border-radius: 3px;">0</code> to disable, or any positive number for the limit</p>
</div>
<div style="padding: 15px;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
<div>
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 12px;">Max Requests per Day</label>
<input type="number" name="max_requests_per_day" style="width: 100%; padding: 12px; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" min="-1" value="{{ tier.max_requests_per_day if tier else '-1' }}">
</div>
<div>
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 12px;">Max Requests per Month</label>
<input type="number" name="max_requests_per_month" style="width: 100%; padding: 12px; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" min="-1" value="{{ tier.max_requests_per_month if tier else '-1' }}">
</div>
<div>
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 12px;">Max Providers</label>
<input type="number" name="max_providers" style="width: 100%; padding: 12px; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" min="-1" value="{{ tier.max_providers if tier else '-1' }}">
</div>
<div>
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 12px;">Max Rotations</label>
<input type="number" name="max_rotations" style="width: 100%; padding: 12px; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" min="-1" value="{{ tier.max_rotations if tier else '-1' }}">
</div>
<div>
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 12px;">Max Autoselections</label>
<input type="number" name="max_autoselections" style="width: 100%; padding: 12px; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" min="-1" value="{{ tier.max_autoselections if tier else '-1' }}">
</div>
<div>
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 12px;">Max Models per Rotation</label>
<input type="number" name="max_rotation_models" style="width: 100%; padding: 12px; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" min="-1" value="{{ tier.max_rotation_models if tier else '-1' }}">
</div>
<div>
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 12px;">Max Models per Autoselection</label>
<input type="number" name="max_autoselection_models" style="width: 100%; padding: 12px; border: 1px solid #0f3460; border-radius: 5px; background: #16213e; color: #e0e0e0; font-size: 14px;" min="-1" value="{{ tier.max_autoselection_models if tier else '-1' }}">
</div>
</div>
</div>
</div>
<div style="margin-bottom: 15px;">
<label style="cursor: pointer; font-weight: 500; color: #e0e0e0;">
<input type="checkbox" name="is_active" value="1" {{ 'checked' if not tier or tier.is_active else '' }} style="margin-right: 10px;">
Tier is active and available for users
</label>
</div>
</div>
<div style="display: flex; gap: 10px;">
<button type="submit" class="btn" style="background: #4a9eff; color: white;">
<i class="fas fa-save me-2"></i>Save Tier
</button>
<a href="{{ url_for(request, '/dashboard/admin/tiers') }}" class="btn" style="background: #6c757d; color: white;">
<i class="fas fa-times me-2"></i>Cancel
</a>
</div>
</form>
</div>
{% endblock %}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -35,15 +35,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -35,15 +35,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<form method="POST" action="{{ url_for(request, '/dashboard/users/add') }}" style="display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end;"> <form method="POST" action="{{ url_for(request, '/dashboard/users/add') }}" style="display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end;">
<div class="form-group" style="flex: 1; min-width: 200px; margin-bottom: 0;"> <div class="form-group" style="flex: 1; min-width: 200px; margin-bottom: 0;">
<label for="username">Username</label> <label for="username">Username</label>
<input type="text" id="username" name="username" required style="width: 100%;"> <input type="text" id="username" name="username" required style="width: 100%; background: #1a1a2e !important; color: #e0e0e0; border: 1px solid #0f3460; padding: 8px; border-radius: 3px;">
</div>
<div class="form-group" style="flex: 1; min-width: 200px; margin-bottom: 0;">
<label for="email">Email</label>
<input type="email" id="email" name="email" style="width: 100%; background: #1a1a2e !important; color: #e0e0e0; border: 1px solid #0f3460; padding: 8px; border-radius: 3px;">
</div> </div>
<div class="form-group" style="flex: 1; min-width: 200px; margin-bottom: 0;"> <div class="form-group" style="flex: 1; min-width: 200px; margin-bottom: 0;">
<label for="password">Password</label> <label for="password">Password</label>
<input type="password" id="password" name="password" required style="width: 100%;"> <input type="password" id="password" name="password" required style="width: 100%; background: #1a1a2e !important; color: #e0e0e0; border: 1px solid #0f3460; padding: 8px; border-radius: 3px;">
</div> </div>
<div class="form-group" style="flex: 0 0 150px; margin-bottom: 0;"> <div class="form-group" style="flex: 0 0 150px; margin-bottom: 0;">
<label for="role">Role</label> <label for="role">Role</label>
<select id="role" name="role" style="width: 100%;"> <select id="role" name="role" style="width: 100%; background: #1a1a2e !important; color: #e0e0e0; border: 1px solid #0f3460; padding: 8px; border-radius: 3px;">
<option value="user">User</option> <option value="user">User</option>
<option value="admin">Admin</option> <option value="admin">Admin</option>
</select> </select>
...@@ -59,6 +63,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -59,6 +63,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>Username</th> <th>Username</th>
<th>Email</th>
<th>Role</th> <th>Role</th>
<th>Created By</th> <th>Created By</th>
<th>Created At</th> <th>Created At</th>
...@@ -73,6 +78,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -73,6 +78,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<tr> <tr>
<td>{{ user.id }}</td> <td>{{ user.id }}</td>
<td>{{ user.username }}</td> <td>{{ user.username }}</td>
<td>{{ user.email or '-' }}</td>
<td> <td>
{% if user.role == 'admin' %} {% if user.role == 'admin' %}
<span style="color: #e94560; font-weight: bold;">Admin</span> <span style="color: #e94560; font-weight: bold;">Admin</span>
...@@ -92,7 +98,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -92,7 +98,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</td> </td>
<td> <td>
<div style="display: flex; gap: 5px; flex-wrap: wrap;"> <div style="display: flex; gap: 5px; flex-wrap: wrap;">
<button onclick="editUser({{ user.id }}, '{{ user.username }}', '{{ user.role }}', {{ user.is_active|lower }})" class="btn btn-secondary" style="padding: 5px 10px; font-size: 12px; margin: 0;">Edit</button> <button onclick="editUser({{ user.id }}, '{{ user.username }}', '{{ user.email }}', '{{ user.role }}', {{ user.is_active|lower }})" class="btn btn-secondary" style="padding: 5px 10px; font-size: 12px; margin: 0;">Edit</button>
<button onclick="toggleUserStatus({{ user.id }}, {{ user.is_active|lower }})" class="btn btn-warning" style="padding: 5px 10px; font-size: 12px; margin: 0;"> <button onclick="toggleUserStatus({{ user.id }}, {{ user.is_active|lower }})" class="btn btn-warning" style="padding: 5px 10px; font-size: 12px; margin: 0;">
{% if user.is_active %}Disable{% else %}Enable{% endif %} {% if user.is_active %}Disable{% else %}Enable{% endif %}
</button> </button>
...@@ -117,11 +123,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -117,11 +123,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<input type="hidden" id="edit-user-id" name="user_id"> <input type="hidden" id="edit-user-id" name="user_id">
<div class="form-group"> <div class="form-group">
<label for="edit-username">Username</label> <label for="edit-username">Username</label>
<input type="text" id="edit-username" name="username" required> <input type="text" id="edit-username" name="username" required style="background: #1a1a2e; color: #e0e0e0; border: 1px solid #0f3460; padding: 8px; border-radius: 3px; width: 100%;">
</div>
<div class="form-group">
<label for="edit-email">Email</label>
<input type="email" id="edit-email" name="email" style="background: #1a1a2e; color: #e0e0e0; border: 1px solid #0f3460; padding: 8px; border-radius: 3px; width: 100%;">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="edit-password">New Password (leave blank to keep current)</label> <label for="edit-password">New Password (leave blank to keep current)</label>
<input type="password" id="edit-password" name="password" placeholder="Enter new password"> <input type="password" id="edit-password" name="password" placeholder="Enter new password" style="background: #1a1a2e; color: #e0e0e0; border: 1px solid #0f3460; padding: 8px; border-radius: 3px; width: 100%;">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="edit-role">Role</label> <label for="edit-role">Role</label>
...@@ -145,9 +155,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -145,9 +155,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div> </div>
<script> <script>
function editUser(userId, username, role, isActive) { function editUser(userId, username, email, role, isActive) {
document.getElementById('edit-user-id').value = userId; document.getElementById('edit-user-id').value = userId;
document.getElementById('edit-username').value = username; document.getElementById('edit-username').value = username;
document.getElementById('edit-email').value = email;
document.getElementById('edit-role').value = role; document.getElementById('edit-role').value = role;
document.getElementById('edit-is-active').checked = isActive; document.getElementById('edit-is-active').checked = isActive;
document.getElementById('edit-form').action = '{{ url_for(request, "/dashboard/users/") }}' + userId + '/edit'; document.getElementById('edit-form').action = '{{ url_for(request, "/dashboard/users/") }}' + userId + '/edit';
...@@ -222,5 +233,13 @@ th { ...@@ -222,5 +233,13 @@ th {
background: #0f3460; background: #0f3460;
font-weight: 600; font-weight: 600;
} }
/* Prevent browser autofill from overriding dark theme */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 30px #1a1a2e inset !important;
-webkit-text-fill-color: #e0e0e0 !important;
}
</style> </style>
{% endblock %} {% endblock %}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment