Commit 84d6f6e4 authored by Your Name's avatar Your Name

feat: Integrate existing database module for multi-user support

- Implement user-specific configuration isolation with SQLite database
- Add user management, authentication, and role-based access control
- Create user-specific providers, rotations, and autoselect configurations
- Add API token management and usage tracking per user
- Update handlers to support user-specific configs with fallback to global
- Add MCP support for user-specific configurations
- Update documentation and README with multi-user features
- Add user dashboard templates for configuration management
parent 4ec3cf51
......@@ -297,15 +297,18 @@ AISBF includes a comprehensive SQLite database system that provides persistent t
The database (`~/.aisbf/aisbf.db`) contains the following tables:
#### Core Tracking Tables
- **`context_dimensions`**: Tracks context size, condensation settings, and effective context per model
- **`token_usage`**: Persistent token usage tracking with TPM/TPH/TPD rate limiting across restarts
- **`model_embeddings`**: Caches model embeddings for semantic classification performance
#### Multi-User Tables
- **`users`**: User management with authentication, roles (admin/user), and metadata
- **`user_providers`**: Isolated provider configurations per user
- **`user_rotations`**: Isolated rotation configurations per user
- **`user_autoselects`**: Isolated autoselect configurations per user
- **`user_providers`**: Isolated provider configurations per user (JSON stored configurations)
- **`user_rotations`**: Isolated rotation configurations per user (JSON stored configurations)
- **`user_autoselects`**: Isolated autoselect configurations per user (JSON stored configurations)
- **`user_api_tokens`**: API token management per user for MCP and API access
- **`user_token_usage`**: Per-user token usage tracking
- **`user_token_usage`**: Per-user token usage tracking and analytics
### Database Initialization
......@@ -317,31 +320,56 @@ The database is automatically initialized on startup:
### Multi-User Support
AISBF supports multiple users with complete isolation:
AISBF supports multiple users with complete isolation through database-backed configurations:
#### User Authentication System
- **Database-First Authentication**: User credentials stored in SQLite database with SHA256 password hashing
- **Config Admin Fallback**: Legacy config-based admin authentication for backward compatibility
- **Role-Based Access Control**: Two roles - `admin` (full access) and `user` (isolated access)
- **Session-Based Authentication**: Secure session management for dashboard access
#### User Isolation Architecture
Each user has completely isolated configurations stored as JSON in the database:
- **Provider Configurations**: `user_providers` table stores individual API keys, endpoints, and model settings
- **Rotation Configurations**: `user_rotations` table stores personal load balancing rules
- **Autoselect Configurations**: `user_autoselects` table stores custom AI-assisted model selection rules
- **API Tokens**: `user_api_tokens` table manages multiple API tokens per user
- **Usage Tracking**: `user_token_usage` table provides per-user analytics and rate limiting
#### Handler Architecture
The system uses user-specific handler instances that load configurations from the database:
```python
# Handler instantiation with user context
request_handler = RequestHandler(user_id=user_id)
rotation_handler = RotationHandler(user_id=user_id)
autoselect_handler = AutoselectHandler(user_id=user_id)
# Automatic loading of user configs
handler.user_providers = db.get_user_providers(user_id)
handler.user_rotations = db.get_user_rotations(user_id)
handler.user_autoselects = db.get_user_autoselects(user_id)
```
#### User Authentication
- Database-first authentication with config admin fallback
- SHA256 password hashing for security
- Role-based access control (admin vs user roles)
- Session-based authentication
#### Configuration Priority
When processing requests, handlers check for user-specific configurations first:
#### User Isolation
- Each user has isolated provider, rotation, and autoselect configurations
- Separate API tokens per user
- Individual token usage tracking
- User-specific dashboard access
1. **User-specific configs** (from database)
2. **Global configs** (from JSON files)
3. **System defaults**
#### Admin Features
- Create/manage users via database
- Full system configuration access
- User management dashboard (future feature)
- Create and manage users via database
- Full access to global configurations and user management
- System-wide analytics and monitoring
- User administration dashboard
#### User Dashboard
- Usage statistics and token tracking
- Personal configuration management
- API token generation and management
- Restricted access to system settings
#### User Dashboard Features
- **Personal Configuration Management**: Create/edit/delete provider, rotation, and autoselect configs
- **Usage Statistics**: Real-time token usage tracking and analytics
- **API Token Management**: Generate, view, and delete API tokens
- **Isolated Access**: No visibility of other users' configurations or system settings
### Persistent Tracking
......
......@@ -40,6 +40,8 @@ Access the dashboard at `http://localhost:17765/dashboard` (default credentials:
- **MCP Server**: Model Context Protocol server for remote agent configuration and model access (SSE and HTTP streaming)
- **Persistent Database**: SQLite-based tracking of token usage, context dimensions, and model embeddings with automatic cleanup
- **Multi-User Support**: User management with isolated configurations, role-based access control, and API token management
- **Database Integration**: SQLite-based persistent storage for user configurations, token usage tracking, and context management
- **User-Specific Configurations**: Each user can have their own providers, rotations, and autoselect configurations stored in the database
## Author
......@@ -293,6 +295,33 @@ When context exceeds the configured percentage of `context_size`, the system aut
See `config/providers.json` and `config/rotations.json` for configuration examples.
### Multi-User Database Integration
AISBF includes comprehensive multi-user support with isolated configurations stored in a SQLite database:
#### User Management
- **Admin Users**: Full access to global configurations and user management
- **Regular Users**: Access to their own configurations and usage statistics
- **Role-Based Access**: Secure separation between admin and user roles
#### Database Features
- **Persistent Storage**: All configurations stored in SQLite database with automatic initialization
- **Token Usage Tracking**: Per-user API token usage statistics and analytics
- **Configuration Isolation**: Each user has separate providers, rotations, and autoselect configurations
- **Automatic Cleanup**: Database maintenance with configurable retention periods
#### User-Specific Configurations
Users can create and manage their own:
- **Providers**: Custom API endpoints, models, and authentication settings
- **Rotations**: Personal load balancing configurations across providers
- **Autoselect**: Custom AI-powered model selection rules
- **API Tokens**: Multiple API tokens with usage tracking and management
#### Dashboard Access
- **Admin Dashboard**: Global configuration management and user administration
- **User Dashboard**: Personal configuration management and usage statistics
- **API Token Management**: Create, view, and delete API tokens with usage analytics
### Content Classification and Semantic Selection
AISBF provides advanced content filtering and intelligent model selection based on content analysis:
......
This diff is collapsed.
......@@ -74,8 +74,24 @@ def generate_system_fingerprint(provider_id: str, seed: Optional[int] = None) ->
return f"fp_{hash_value}"
class RequestHandler:
def __init__(self):
def __init__(self, user_id=None):
self.user_id = user_id
self.config = config
# Load user-specific configs if user_id is provided
if user_id:
self._load_user_configs()
else:
self.user_providers = {}
self.user_rotations = {}
self.user_autoselects = {}
def _load_user_configs(self):
"""Load user-specific configurations from database"""
from .database import get_database
db = get_database()
self.user_providers = db.get_user_providers(self.user_id)
self.user_rotations = db.get_user_rotations(self.user_id)
self.user_autoselects = db.get_user_autoselects(self.user_id)
async def _handle_chunked_request(
self,
......@@ -210,9 +226,17 @@ class RequestHandler:
logger = logging.getLogger(__name__)
logger.info(f"=== RequestHandler.handle_chat_completion START ===")
logger.info(f"Provider ID: {provider_id}")
logger.info(f"User ID: {self.user_id}")
logger.info(f"Request data: {request_data}")
provider_config = self.config.get_provider(provider_id)
# Check for user-specific provider config first
if self.user_id and provider_id in self.user_providers:
provider_config = self.user_providers[provider_id]
logger.info(f"Using user-specific provider config for {provider_id}")
else:
provider_config = self.config.get_provider(provider_id)
logger.info(f"Using global provider config for {provider_id}")
logger.info(f"Provider config: {provider_config}")
logger.info(f"Provider type: {provider_config.type}")
logger.info(f"Provider endpoint: {provider_config.endpoint}")
......@@ -338,7 +362,11 @@ class RequestHandler:
raise HTTPException(status_code=500, detail=str(e))
async def handle_streaming_chat_completion(self, request: Request, provider_id: str, request_data: Dict):
provider_config = self.config.get_provider(provider_id)
# Check for user-specific provider config first
if self.user_id and provider_id in self.user_providers:
provider_config = self.user_providers[provider_id]
else:
provider_config = self.config.get_provider(provider_id)
if provider_config.api_key_required:
api_key = request_data.get('api_key') or request.headers.get('Authorization', '').replace('Bearer ', '')
......@@ -1214,8 +1242,24 @@ class RequestHandler:
raise HTTPException(status_code=500, detail=f"Error fetching content: {str(e)}")
class RotationHandler:
def __init__(self):
def __init__(self, user_id=None):
self.user_id = user_id
self.config = config
# Load user-specific configs if user_id is provided
if user_id:
self._load_user_configs()
else:
self.user_providers = {}
self.user_rotations = {}
self.user_autoselects = {}
def _load_user_configs(self):
"""Load user-specific configurations from database"""
from .database import get_database
db = get_database()
self.user_providers = db.get_user_providers(self.user_id)
self.user_rotations = db.get_user_rotations(self.user_id)
self.user_autoselects = db.get_user_autoselects(self.user_id)
def _get_provider_type(self, provider_id: str) -> str:
"""Get the provider type from configuration"""
......@@ -1435,7 +1479,7 @@ class RotationHandler:
async def handle_rotation_request(self, rotation_id: str, request_data: Dict):
"""
Handle a rotation request.
For streaming requests, returns a StreamingResponse with proper handling
based on the selected provider's type (google vs others).
For non-streaming requests, returns the response dict directly.
......@@ -1445,8 +1489,16 @@ class RotationHandler:
logger = logging.getLogger(__name__)
logger.info(f"=== RotationHandler.handle_rotation_request START ===")
logger.info(f"Rotation ID: {rotation_id}")
rotation_config = self.config.get_rotation(rotation_id)
logger.info(f"User ID: {self.user_id}")
# Check for user-specific rotation config first
if self.user_id and rotation_id in self.user_rotations:
rotation_config = self.user_rotations[rotation_id]
logger.info(f"Using user-specific rotation config for {rotation_id}")
else:
rotation_config = self.config.get_rotation(rotation_id)
logger.info(f"Using global rotation config for {rotation_id}")
if not rotation_config:
logger.error(f"Rotation {rotation_id} not found")
raise HTTPException(status_code=400, detail=f"Rotation {rotation_id} not found")
......@@ -1476,11 +1528,17 @@ class RotationHandler:
logger.info(f"")
logger.info(f"--- Processing provider: {provider_id} ---")
# Check if provider exists in configuration
provider_config = self.config.get_provider(provider_id)
# Check if provider exists in configuration (user-specific first, then global)
if self.user_id and provider_id in self.user_providers:
provider_config = self.user_providers[provider_id]
logger.info(f" [USER] Using user-specific provider config for {provider_id}")
else:
provider_config = self.config.get_provider(provider_id)
logger.info(f" [GLOBAL] Using global provider config for {provider_id}")
if not provider_config:
logger.error(f" [ERROR] Provider {provider_id} not found in providers configuration")
logger.error(f" Available providers: {list(self.config.providers.keys())}")
logger.error(f" Available providers: {list(self.config.providers.keys()) if not self.user_id else list(self.user_providers.keys())}")
logger.error(f" Skipping this provider")
skipped_providers.append(provider_id)
continue
......@@ -2760,12 +2818,28 @@ class RotationHandler:
return capabilities
class AutoselectHandler:
def __init__(self):
def __init__(self, user_id=None):
self.user_id = user_id
self.config = config
self._skill_file_content = None
self._internal_model = None
self._internal_tokenizer = None
self._internal_model_lock = None
# Load user-specific configs if user_id is provided
if user_id:
self._load_user_configs()
else:
self.user_providers = {}
self.user_rotations = {}
self.user_autoselects = {}
def _load_user_configs(self):
"""Load user-specific configurations from database"""
from .database import get_database
db = get_database()
self.user_providers = db.get_user_providers(self.user_id)
self.user_rotations = db.get_user_rotations(self.user_id)
self.user_autoselects = db.get_user_autoselects(self.user_id)
def _get_skill_file_content(self) -> str:
"""Load the autoselect.md skill file content"""
......@@ -3130,8 +3204,16 @@ class AutoselectHandler:
logger = logging.getLogger(__name__)
logger.info(f"=== AUTOSELECT REQUEST START ===")
logger.info(f"Autoselect ID: {autoselect_id}")
autoselect_config = self.config.get_autoselect(autoselect_id)
logger.info(f"User ID: {self.user_id}")
# Check for user-specific autoselect config first
if self.user_id and autoselect_id in self.user_autoselects:
autoselect_config = self.user_autoselects[autoselect_id]
logger.info(f"Using user-specific autoselect config for {autoselect_id}")
else:
autoselect_config = self.config.get_autoselect(autoselect_id)
logger.info(f"Using global autoselect config for {autoselect_id}")
if not autoselect_config:
logger.error(f"Autoselect {autoselect_id} not found")
raise HTTPException(status_code=400, detail=f"Autoselect {autoselect_id} not found")
......
......@@ -427,7 +427,7 @@ class MCPServer:
return tools
async def handle_tool_call(self, tool_name: str, arguments: Dict, auth_level: int) -> Dict:
async def handle_tool_call(self, tool_name: str, arguments: Dict, auth_level: int, user_id: Optional[int] = None) -> Dict:
"""
Handle an MCP tool call.
......@@ -476,7 +476,7 @@ class MCPServer:
raise HTTPException(status_code=404, detail=f"Tool '{tool_name}' not found")
handler = handlers[tool_name]
return await handler(arguments)
return await handler(arguments, user_id)
async def _list_models(self, args: Dict) -> Dict:
"""List all available models"""
......@@ -562,18 +562,18 @@ class MCPServer:
}
return {"autoselect": autoselect_info}
async def _chat_completion(self, args: Dict) -> Dict:
async def _chat_completion(self, args: Dict, user_id: Optional[int] = None) -> Dict:
"""Handle chat completion request"""
from .handlers import RequestHandler, RotationHandler, AutoselectHandler
from .models import ChatCompletionRequest
from starlette.requests import Request
model = args.get('model')
messages = args.get('messages', [])
temperature = args.get('temperature', 1.0)
max_tokens = args.get('max_tokens', 2048)
stream = args.get('stream', False)
# Parse provider from model
if '/' in model:
parts = model.split('/', 1)
......@@ -582,7 +582,7 @@ class MCPServer:
else:
provider_id = model
actual_model = model
# Create request data
request_data = {
"model": actual_model,
......@@ -591,7 +591,7 @@ class MCPServer:
"max_tokens": max_tokens,
"stream": stream
}
# Create dummy request
scope = {
"type": "http",
......@@ -601,25 +601,26 @@ class MCPServer:
"path": f"/api/{provider_id}/chat/completions"
}
dummy_request = Request(scope)
# Route to appropriate handler
# Route to appropriate handler (with user_id support)
from main import get_user_handler
if provider_id == "autoselect":
if actual_model not in self.config.autoselect:
handler = get_user_handler('autoselect', user_id)
if actual_model not in self.config.autoselect and (not user_id or actual_model not in handler.user_autoselects):
raise HTTPException(status_code=400, detail=f"Autoselect '{actual_model}' not found")
handler = AutoselectHandler()
if stream:
return {"error": "Streaming not supported in MCP, use SSE endpoint instead"}
else:
return await handler.handle_autoselect_request(actual_model, request_data)
elif provider_id == "rotation":
if actual_model not in self.config.rotations:
handler = get_user_handler('rotation', user_id)
if actual_model not in self.config.rotations and (not user_id or actual_model not in handler.user_rotations):
raise HTTPException(status_code=400, detail=f"Rotation '{actual_model}' not found")
handler = RotationHandler()
return await handler.handle_rotation_request(actual_model, request_data)
else:
if provider_id not in self.config.providers:
handler = get_user_handler('request', user_id)
if provider_id not in self.config.providers and (not user_id or provider_id not in handler.user_providers):
raise HTTPException(status_code=400, detail=f"Provider '{provider_id}' not found")
handler = RequestHandler()
if stream:
return {"error": "Streaming not supported in MCP, use SSE endpoint instead"}
else:
......
This diff is collapsed.
{% extends "base.html" %}
{% block title %}My Autoselects - AISBF{% endblock %}
{% block content %}
<div class="container">
<h1>My Autoselects</h1>
<p>Manage your personal autoselect configurations</p>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<div class="card">
<h2>Autoselect Configurations</h2>
<div id="autoselects-list">
<!-- Autoselects will be loaded here -->
</div>
<button class="btn btn-primary" onclick="showAddAutoselectModal()">Add New Autoselect</button>
</div>
</div>
<!-- Modal for adding/editing autoselects -->
<div id="autoselect-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 id="modal-title">Add Autoselect</h3>
<button class="close-btn" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<form id="autoselect-form">
<div class="form-group">
<label for="autoselect-name">Autoselect Name:</label>
<input type="text" id="autoselect-name" required>
</div>
<div class="form-group">
<label for="autoselect-config">Autoselect Configuration (JSON):</label>
<textarea id="autoselect-config" rows="20" placeholder='{"model_name": "autoselect-name", "description": "My autoselect config", "fallback": "provider/model"}'></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<script>
let autoselects = {{ user_autoselects_json | safe }};
let currentEditingIndex = -1;
function renderAutoselects() {
const container = document.getElementById('autoselects-list');
container.innerHTML = '';
if (autoselects.length === 0) {
container.innerHTML = '<p class="empty-state">No autoselects configured yet. Click "Add New Autoselect" to get started.</p>';
return;
}
autoselects.forEach((autoselect, index) => {
const div = document.createElement('div');
div.className = 'autoselect-item';
div.innerHTML = `
<div class="autoselect-header">
<h3>${autoselect.autoselect_id}</h3>
<div class="autoselect-actions">
<button class="btn btn-secondary btn-sm" onclick="editAutoselect(${index})">Edit</button>
<button class="btn btn-danger btn-sm" onclick="deleteAutoselect('${autoselect.autoselect_id}')">Delete</button>
</div>
</div>
<div class="autoselect-details">
<p><strong>Created:</strong> ${new Date(autoselect.created_at).toLocaleString()}</p>
<p><strong>Last Updated:</strong> ${new Date(autoselect.updated_at).toLocaleString()}</p>
<details>
<summary>Configuration (JSON)</summary>
<pre>${JSON.stringify(autoselect.config, null, 2)}</pre>
</details>
</div>
`;
container.appendChild(div);
});
}
function showAddAutoselectModal() {
currentEditingIndex = -1;
document.getElementById('modal-title').textContent = 'Add Autoselect';
document.getElementById('autoselect-name').value = '';
document.getElementById('autoselect-config').value = '{"model_name": "my-autoselect", "description": "My autoselect configuration", "fallback": "provider/model"}';
document.getElementById('autoselect-modal').style.display = 'block';
}
function editAutoselect(index) {
currentEditingIndex = index;
const autoselect = autoselects[index];
document.getElementById('modal-title').textContent = 'Edit Autoselect';
document.getElementById('autoselect-name').value = autoselect.autoselect_id;
document.getElementById('autoselect-name').readOnly = true;
document.getElementById('autoselect-config').value = JSON.stringify(autoselect.config, null, 2);
document.getElementById('autoselect-modal').style.display = 'block';
}
function closeModal() {
document.getElementById('autoselect-modal').style.display = 'none';
document.getElementById('autoselect-name').readOnly = false;
currentEditingIndex = -1;
}
function deleteAutoselect(autoselectName) {
if (!confirm(`Are you sure you want to delete the autoselect "${autoselectName}"? This action cannot be undone.`)) return;
fetch('{{ url_for(request, "/dashboard/user/autoselects") }}/' + encodeURIComponent(autoselectName), {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
if (response.ok) {
location.reload();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to delete autoselect');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
}
// Handle form submission
document.getElementById('autoselect-form').addEventListener('submit', function(e) {
e.preventDefault();
const autoselectName = document.getElementById('autoselect-name').value.trim();
const configText = document.getElementById('autoselect-config').value.trim();
if (!autoselectName) {
alert('Autoselect name is required');
return;
}
if (!configText) {
alert('Autoselect configuration is required');
return;
}
let configObj;
try {
configObj = JSON.parse(configText);
} catch (e) {
alert('Invalid JSON configuration: ' + e.message);
return;
}
const formData = new FormData();
formData.append('autoselect_name', autoselectName);
formData.append('autoselect_config', JSON.stringify(configObj));
const url = '{{ url_for(request, "/dashboard/user/autoselects") }}';
const method = currentEditingIndex >= 0 ? 'PUT' : 'POST';
fetch(url, {
method: method,
body: formData
}).then(response => {
if (response.ok) {
closeModal();
location.reload();
} else {
return response.text().then(text => {
throw new Error(text || 'Failed to save autoselect');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
});
renderAutoselects();
</script>
<style>
.autoselect-item {
background: #1a1a2e;
padding: 1rem;
margin: 1rem 0;
border-radius: 8px;
}
.autoselect-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.autoselect-header h3 {
margin: 0;
}
.autoselect-actions {
display: flex;
gap: 0.5rem;
}
.autoselect-details p {
margin: 0.5rem 0;
}
.autoselect-details pre {
background: #0f3460;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.empty-state {
text-align: center;
color: #a0a0a0;
padding: 2rem;
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: #1a1a2e;
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #0f3460;
}
.modal-header h3 {
margin: 0;
}
.close-btn {
background: none;
border: none;
color: #e0e0e0;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-body {
padding: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #e0e0e0;
}
.form-group input[type="text"],
.form-group textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid #0f3460;
border-radius: 4px;
background: #16213e;
color: #e0e0e0;
font-family: monospace;
font-size: 14px;
}
.form-group textarea {
resize: vertical;
min-height: 300px;
}
.form-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
margin-top: 1rem;
}
</style>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title %}User Dashboard - AISBF{% endblock %}
{% block content %}
<div class="container">
<h1>User Dashboard</h1>
<p>Welcome, {{ session.username }}!</p>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<!-- Usage Statistics -->
<div class="card">
<h2>Usage Statistics</h2>
<div class="stats-grid">
<div class="stat-item">
<h3>Total Tokens Used</h3>
<p class="stat-value">{{ usage_stats.total_tokens|default(0) }}</p>
</div>
<div class="stat-item">
<h3>Requests Today</h3>
<p class="stat-value">{{ usage_stats.requests_today|default(0) }}</p>
</div>
<div class="stat-item">
<h3>Active Providers</h3>
<p class="stat-value">{{ providers_count|default(0) }}</p>
</div>
<div class="stat-item">
<h3>Active Rotations</h3>
<p class="stat-value">{{ rotations_count|default(0) }}</p>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card">
<h2>Quick Actions</h2>
<div class="button-group">
<a href="{{ url_for(request, '/dashboard/user/providers') }}" class="btn btn-primary">Manage Providers</a>
<a href="{{ url_for(request, '/dashboard/user/rotations') }}" class="btn btn-primary">Manage Rotations</a>
<a href="{{ url_for(request, '/dashboard/user/autoselect') }}" class="btn btn-primary">Manage Autoselect</a>
<a href="{{ url_for(request, '/dashboard/user/tokens') }}" class="btn btn-primary">API Tokens</a>
</div>
</div>
<!-- Recent Activity -->
<div class="card">
<h2>Recent Activity</h2>
<table class="table">
<thead>
<tr>
<th>Timestamp</th>
<th>Provider</th>
<th>Model</th>
<th>Tokens</th>
</tr>
</thead>
<tbody>
{% if recent_activity %}
{% for activity in recent_activity %}
<tr>
<td>{{ activity.timestamp }}</td>
<td>{{ activity.provider_id }}</td>
<td>{{ activity.model }}</td>
<td>{{ activity.token_count }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="4" style="text-align: center;">No recent activity</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<style>
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.stat-item {
background: #1a1a2e;
padding: 1.5rem;
border-radius: 8px;
text-align: center;
}
.stat-item h3 {
font-size: 0.9rem;
color: #888;
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
color: #16213e;
}
.button-group {
display: flex;
gap: 1rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.table {
width: 100%;
margin-top: 1rem;
border-collapse: collapse;
}
.table th,
.table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #333;
}
.table th {
background: #1a1a2e;
font-weight: bold;
}
.table tbody tr:hover {
background: #1a1a2e;
}
</style>
{% endblock %}
{% extends "base.html" %}
{% block title %}My Providers - AISBF{% endblock %}
{% block content %}
<div class="container">
<h1>My Providers</h1>
<p>Manage your personal provider configurations</p>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<div class="card">
<h2>Provider Configurations</h2>
<div id="providers-list">
<!-- Providers will be loaded here -->
</div>
<button class="btn btn-primary" onclick="showAddProviderModal()">Add New Provider</button>
</div>
</div>
<!-- Modal for adding/editing providers -->
<div id="provider-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 id="modal-title">Add Provider</h3>
<button class="close-btn" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<form id="provider-form">
<div class="form-group">
<label for="provider-name">Provider Name:</label>
<input type="text" id="provider-name" required>
</div>
<div class="form-group">
<label for="provider-config">Provider Configuration (JSON):</label>
<textarea id="provider-config" rows="20" placeholder='{"endpoint": "https://api.example.com", "api_key": "your-api-key", "models": []}'></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<script>
let providers = {{ user_providers_json | safe }};
let currentEditingIndex = -1;
function renderProviders() {
const container = document.getElementById('providers-list');
container.innerHTML = '';
if (providers.length === 0) {
container.innerHTML = '<p class="empty-state">No providers configured yet. Click "Add New Provider" to get started.</p>';
return;
}
providers.forEach((provider, index) => {
const div = document.createElement('div');
div.className = 'provider-item';
div.innerHTML = `
<div class="provider-header">
<h3>${provider.provider_id}</h3>
<div class="provider-actions">
<button class="btn btn-secondary btn-sm" onclick="editProvider(${index})">Edit</button>
<button class="btn btn-danger btn-sm" onclick="deleteProvider('${provider.provider_id}')">Delete</button>
</div>
</div>
<div class="provider-details">
<p><strong>Created:</strong> ${new Date(provider.created_at).toLocaleString()}</p>
<p><strong>Last Updated:</strong> ${new Date(provider.updated_at).toLocaleString()}</p>
<details>
<summary>Configuration (JSON)</summary>
<pre>${JSON.stringify(provider.config, null, 2)}</pre>
</details>
</div>
`;
container.appendChild(div);
});
}
function showAddProviderModal() {
currentEditingIndex = -1;
document.getElementById('modal-title').textContent = 'Add Provider';
document.getElementById('provider-name').value = '';
document.getElementById('provider-config').value = '{"endpoint": "https://api.example.com", "api_key": "your-api-key", "models": []}';
document.getElementById('provider-modal').style.display = 'block';
}
function editProvider(index) {
currentEditingIndex = index;
const provider = providers[index];
document.getElementById('modal-title').textContent = 'Edit Provider';
document.getElementById('provider-name').value = provider.provider_id;
document.getElementById('provider-name').readOnly = true;
document.getElementById('provider-config').value = JSON.stringify(provider.config, null, 2);
document.getElementById('provider-modal').style.display = 'block';
}
function closeModal() {
document.getElementById('provider-modal').style.display = 'none';
document.getElementById('provider-name').readOnly = false;
currentEditingIndex = -1;
}
function deleteProvider(providerName) {
if (!confirm(`Are you sure you want to delete the provider "${providerName}"? This action cannot be undone.`)) return;
fetch('{{ url_for(request, "/dashboard/user/providers") }}/' + encodeURIComponent(providerName), {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
if (response.ok) {
location.reload();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to delete provider');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
}
// Handle form submission
document.getElementById('provider-form').addEventListener('submit', function(e) {
e.preventDefault();
const providerName = document.getElementById('provider-name').value.trim();
const configText = document.getElementById('provider-config').value.trim();
if (!providerName) {
alert('Provider name is required');
return;
}
if (!configText) {
alert('Provider configuration is required');
return;
}
let configObj;
try {
configObj = JSON.parse(configText);
} catch (e) {
alert('Invalid JSON configuration: ' + e.message);
return;
}
const formData = new FormData();
formData.append('provider_name', providerName);
formData.append('provider_config', JSON.stringify(configObj));
const url = '{{ url_for(request, "/dashboard/user/providers") }}';
const method = currentEditingIndex >= 0 ? 'PUT' : 'POST';
fetch(url, {
method: method,
body: formData
}).then(response => {
if (response.ok) {
closeModal();
location.reload();
} else {
return response.text().then(text => {
throw new Error(text || 'Failed to save provider');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
});
renderProviders();
</script>
<style>
.provider-item {
background: #1a1a2e;
padding: 1rem;
margin: 1rem 0;
border-radius: 8px;
}
.provider-item h3 {
margin-top: 0;
}
.provider-item code {
background: #0f3460;
padding: 0.2rem 0.5rem;
border-radius: 4px;
}
</style>
{% endblock %}
{% extends "base.html" %}
{% block title %}My Rotations - AISBF{% endblock %}
{% block content %}
<div class="container">
<h1>My Rotations</h1>
<p>Manage your personal rotation configurations</p>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<div class="card">
<h2>Rotation Configurations</h2>
<div id="rotations-list">
<!-- Rotations will be loaded here -->
</div>
<button class="btn btn-primary" onclick="showAddRotationModal()">Add New Rotation</button>
</div>
</div>
<!-- Modal for adding/editing rotations -->
<div id="rotation-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 id="modal-title">Add Rotation</h3>
<button class="close-btn" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<form id="rotation-form">
<div class="form-group">
<label for="rotation-name">Rotation Name:</label>
<input type="text" id="rotation-name" required>
</div>
<div class="form-group">
<label for="rotation-config">Rotation Configuration (JSON):</label>
<textarea id="rotation-config" rows="20" placeholder='{"model_name": "rotation-name", "providers": []}'></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<script>
let rotations = {{ user_rotations_json | safe }};
let currentEditingIndex = -1;
function renderRotations() {
const container = document.getElementById('rotations-list');
container.innerHTML = '';
if (rotations.length === 0) {
container.innerHTML = '<p class="empty-state">No rotations configured yet. Click "Add New Rotation" to get started.</p>';
return;
}
rotations.forEach((rotation, index) => {
const div = document.createElement('div');
div.className = 'rotation-item';
div.innerHTML = `
<div class="rotation-header">
<h3>${rotation.rotation_id}</h3>
<div class="rotation-actions">
<button class="btn btn-secondary btn-sm" onclick="editRotation(${index})">Edit</button>
<button class="btn btn-danger btn-sm" onclick="deleteRotation('${rotation.rotation_id}')">Delete</button>
</div>
</div>
<div class="rotation-details">
<p><strong>Created:</strong> ${new Date(rotation.created_at).toLocaleString()}</p>
<p><strong>Last Updated:</strong> ${new Date(rotation.updated_at).toLocaleString()}</p>
<details>
<summary>Configuration (JSON)</summary>
<pre>${JSON.stringify(rotation.config, null, 2)}</pre>
</details>
</div>
`;
container.appendChild(div);
});
}
function showAddRotationModal() {
currentEditingIndex = -1;
document.getElementById('modal-title').textContent = 'Add Rotation';
document.getElementById('rotation-name').value = '';
document.getElementById('rotation-config').value = '{"model_name": "my-rotation", "providers": []}';
document.getElementById('rotation-modal').style.display = 'block';
}
function editRotation(index) {
currentEditingIndex = index;
const rotation = rotations[index];
document.getElementById('modal-title').textContent = 'Edit Rotation';
document.getElementById('rotation-name').value = rotation.rotation_id;
document.getElementById('rotation-name').readOnly = true;
document.getElementById('rotation-config').value = JSON.stringify(rotation.config, null, 2);
document.getElementById('rotation-modal').style.display = 'block';
}
function closeModal() {
document.getElementById('rotation-modal').style.display = 'none';
document.getElementById('rotation-name').readOnly = false;
currentEditingIndex = -1;
}
function deleteRotation(rotationName) {
if (!confirm(`Are you sure you want to delete the rotation "${rotationName}"? This action cannot be undone.`)) return;
fetch('{{ url_for(request, "/dashboard/user/rotations") }}/' + encodeURIComponent(rotationName), {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
if (response.ok) {
location.reload();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to delete rotation');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
}
// Handle form submission
document.getElementById('rotation-form').addEventListener('submit', function(e) {
e.preventDefault();
const rotationName = document.getElementById('rotation-name').value.trim();
const configText = document.getElementById('rotation-config').value.trim();
if (!rotationName) {
alert('Rotation name is required');
return;
}
if (!configText) {
alert('Rotation configuration is required');
return;
}
let configObj;
try {
configObj = JSON.parse(configText);
} catch (e) {
alert('Invalid JSON configuration: ' + e.message);
return;
}
const formData = new FormData();
formData.append('rotation_name', rotationName);
formData.append('rotation_config', JSON.stringify(configObj));
const url = '{{ url_for(request, "/dashboard/user/rotations") }}';
const method = currentEditingIndex >= 0 ? 'PUT' : 'POST';
fetch(url, {
method: method,
body: formData
}).then(response => {
if (response.ok) {
closeModal();
location.reload();
} else {
return response.text().then(text => {
throw new Error(text || 'Failed to save rotation');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
});
renderRotations();
</script>
<style>
.rotation-item {
background: #1a1a2e;
padding: 1rem;
margin: 1rem 0;
border-radius: 8px;
}
.rotation-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.rotation-header h3 {
margin: 0;
}
.rotation-actions {
display: flex;
gap: 0.5rem;
}
.rotation-details p {
margin: 0.5rem 0;
}
.rotation-details pre {
background: #0f3460;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.empty-state {
text-align: center;
color: #a0a0a0;
padding: 2rem;
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: #1a1a2e;
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #0f3460;
}
.modal-header h3 {
margin: 0;
}
.close-btn {
background: none;
border: none;
color: #e0e0e0;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-body {
padding: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #e0e0e0;
}
.form-group input[type="text"],
.form-group textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid #0f3460;
border-radius: 4px;
background: #16213e;
color: #e0e0e0;
font-family: monospace;
font-size: 14px;
}
.form-group textarea {
resize: vertical;
min-height: 300px;
}
.form-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
margin-top: 1rem;
}
</style>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title %}My API Tokens - AISBF{% endblock %}
{% block content %}
<div class="container">
<h1>My API Tokens</h1>
<p>Manage your personal API tokens for accessing the AISBF API</p>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<div class="card">
<h2>API Tokens</h2>
<div id="tokens-list">
<!-- Tokens will be loaded here -->
</div>
<button class="btn btn-primary" onclick="createToken()">Create New Token</button>
</div>
<!-- Modal for creating tokens -->
<div id="token-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>Create New API Token</h3>
<button class="close-btn" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<div id="token-creation-form">
<form id="create-token-form">
<div class="form-group">
<label for="token-description">Description (optional):</label>
<input type="text" id="token-description" placeholder="e.g., My application token">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Create Token</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
</div>
</form>
</div>
<div id="token-result" style="display: none;">
<div class="alert alert-success">
<h4>Token Created Successfully!</h4>
<p><strong>Token:</strong> <code id="created-token"></code></p>
<p class="warning-text">⚠️ <strong>Important:</strong> Copy this token now. You won't be able to see it again!</p>
<div class="form-actions">
<button type="button" class="btn btn-primary" onclick="copyToken()">Copy Token</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Close</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let tokens = {{ user_tokens | tojson }};
function renderTokens() {
const container = document.getElementById('tokens-list');
container.innerHTML = '';
if (tokens.length === 0) {
container.innerHTML = '<p class="empty-state">No API tokens created yet. Click "Create New Token" to get started.</p>';
return;
}
tokens.forEach((token) => {
const div = document.createElement('div');
div.className = 'token-item';
div.innerHTML = `
<div class="token-header">
<div class="token-info">
<h4>${token.description || 'No description'}</h4>
<p class="token-meta">
Created: ${new Date(token.created_at).toLocaleString()}
${token.last_used ? `| Last used: ${new Date(token.last_used).toLocaleString()}` : ''}
</p>
</div>
<div class="token-actions">
<span class="token-status ${token.is_active ? 'active' : 'inactive'}">
${token.is_active ? 'Active' : 'Inactive'}
</span>
<button class="btn btn-danger btn-sm" onclick="deleteToken(${token.id})">Delete</button>
</div>
</div>
<div class="token-details">
<p><strong>Token ID:</strong> ${token.id}</p>
<p><strong>Token:</strong> ${token.token.substring(0, 20)}...</p>
</div>
`;
container.appendChild(div);
});
}
function createToken() {
document.getElementById('token-creation-form').style.display = 'block';
document.getElementById('token-result').style.display = 'none';
document.getElementById('create-token-form').reset();
document.getElementById('token-modal').style.display = 'block';
}
function closeModal() {
document.getElementById('token-modal').style.display = 'none';
}
function deleteToken(tokenId) {
if (!confirm('Are you sure you want to delete this API token? This action cannot be undone and will immediately revoke access.')) return;
fetch('{{ url_for(request, "/dashboard/user/tokens") }}/' + tokenId, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
if (response.ok) {
location.reload();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to delete token');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
}
function copyToken() {
const tokenElement = document.getElementById('created-token');
const token = tokenElement.textContent;
navigator.clipboard.writeText(token).then(() => {
alert('Token copied to clipboard!');
}).catch(() => {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = token;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
alert('Token copied to clipboard!');
});
}
// Handle form submission
document.getElementById('create-token-form').addEventListener('submit', function(e) {
e.preventDefault();
const description = document.getElementById('token-description').value.trim();
const formData = new FormData();
if (description) {
formData.append('description', description);
}
fetch('{{ url_for(request, "/dashboard/user/tokens") }}', {
method: 'POST',
body: formData
}).then(response => {
if (response.ok) {
return response.json();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to create token');
});
}
}).then(data => {
// Show the created token
document.getElementById('created-token').textContent = data.token;
document.getElementById('token-creation-form').style.display = 'none';
document.getElementById('token-result').style.display = 'block';
// Refresh the token list
tokens.push({
id: data.token_id,
token: data.token,
description: description || null,
created_at: new Date().toISOString(),
last_used: null,
is_active: true
});
renderTokens();
}).catch(error => {
alert('Error: ' + error.message);
});
});
renderTokens();
</script>
<style>
.token-item {
background: #1a1a2e;
padding: 1rem;
margin: 1rem 0;
border-radius: 8px;
border: 1px solid #0f3460;
}
.token-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.token-info h4 {
margin: 0 0 0.5rem 0;
color: #e0e0e0;
}
.token-meta {
color: #a0a0a0;
font-size: 0.9rem;
margin: 0;
}
.token-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.token-status {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
}
.token-status.active {
background: #28a745;
color: white;
}
.token-status.inactive {
background: #dc3545;
color: white;
}
.token-details {
background: #16213e;
padding: 1rem;
border-radius: 4px;
font-family: monospace;
font-size: 0.9rem;
}
.token-details p {
margin: 0.5rem 0;
word-break: break-all;
}
.empty-state {
text-align: center;
color: #a0a0a0;
padding: 2rem;
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: #1a1a2e;
border-radius: 8px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #0f3460;
}
.modal-header h3 {
margin: 0;
}
.close-btn {
background: none;
border: none;
color: #e0e0e0;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-body {
padding: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #e0e0e0;
}
.form-group input[type="text"] {
width: 100%;
padding: 0.5rem;
border: 1px solid #0f3460;
border-radius: 4px;
background: #16213e;
color: #e0e0e0;
font-size: 14px;
}
.form-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
margin-top: 1rem;
}
.warning-text {
color: #ffc107;
font-weight: 500;
}
#created-token {
background: #0f3460;
padding: 0.5rem;
border-radius: 4px;
font-family: monospace;
word-break: break-all;
}
</style>
{% 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