Commit 3458d0f7 authored by Your Name's avatar Your Name

Release 0.9.6

parent ccc498ee
...@@ -738,6 +738,14 @@ This AI.PROMPT file is automatically updated when significant changes are made t ...@@ -738,6 +738,14 @@ This AI.PROMPT file is automatically updated when significant changes are made t
### Recent Updates ### Recent Updates
**2026-04-03 - Version 0.9.6 - Fixed aisbf.json Installation Path**
- Fixed aisbf.json not being copied to ~/.aisbf/ directory on first run
- Updated _ensure_config_directory() in aisbf/config.py to include aisbf.json in the list of files to copy
- Previously only providers.json, rotations.json, and autoselect.json were copied
- This caused aisbf.json to remain in ~/.local/share/aisbf/ instead of being accessible in ~/.aisbf/
- Now all configuration files are properly copied to the user's home directory for easy editing
- Version bumped to 0.9.6 in setup.py and pyproject.toml
**2026-04-03 - Version 0.9.4 - Setup.py Fixes and Documentation Updates** **2026-04-03 - Version 0.9.4 - Setup.py Fixes and Documentation Updates**
- Fixed setup.py data_files structure to preserve subdirectory structure during installation - Fixed setup.py data_files structure to preserve subdirectory structure during installation
- Split data_files into separate entries for each subpackage (aisbf/, aisbf/providers/, aisbf/providers/kiro/, aisbf/auth/) - Split data_files into separate entries for each subpackage (aisbf/, aisbf/providers/, aisbf/providers/kiro/, aisbf/auth/)
......
...@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ...@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.9.6] - 2026-04-03
### Fixed
- **aisbf.json Installation Path**: Fixed aisbf.json not being copied to ~/.aisbf/ directory on first run
- Updated _ensure_config_directory() in aisbf/config.py to include aisbf.json in the list of files to copy
- Previously only providers.json, rotations.json, and autoselect.json were copied
- This caused aisbf.json to remain in ~/.local/share/aisbf/ instead of being accessible in ~/.aisbf/
- Now all configuration files are properly copied to the user's home directory for easy editing
### Changed
- **Version Bump**: Updated version to 0.9.6 in setup.py and pyproject.toml
## [0.9.4] - 2026-04-03 ## [0.9.4] - 2026-04-03
### Added ### Added
......
...@@ -302,7 +302,7 @@ class Config: ...@@ -302,7 +302,7 @@ class Config:
return return
# Copy default config files if they don't exist # Copy default config files if they don't exist
for config_file in ['providers.json', 'rotations.json', 'autoselect.json']: for config_file in ['providers.json', 'rotations.json', 'autoselect.json', 'aisbf.json']:
src = source_dir / config_file src = source_dir / config_file
dst = config_dir / config_file dst = config_dir / config_file
......
...@@ -471,6 +471,8 @@ def setup_template_globals(): ...@@ -471,6 +471,8 @@ def setup_template_globals():
"""Setup Jinja2 template globals for proxy-aware URLs""" """Setup Jinja2 template globals for proxy-aware URLs"""
templates.env.globals['url_for'] = url_for templates.env.globals['url_for'] = url_for
templates.env.globals['get_base_url'] = get_base_url templates.env.globals['get_base_url'] = get_base_url
# Clear the template cache to avoid stale cache issues
templates.env.cache.clear()
# Call setup after templates are initialized # Call setup after templates are initialized
setup_template_globals() setup_template_globals()
...@@ -941,7 +943,7 @@ async def startup_event(): ...@@ -941,7 +943,7 @@ async def startup_event():
# Check 2: OAuth2 credentials file # Check 2: OAuth2 credentials file
if not has_valid_auth: if not has_valid_auth:
try: try:
from aisbf.kilo_oauth2 import KiloOAuth2 from aisbf.auth.kilo import KiloOAuth2
kilo_config = getattr(provider_config, 'kilo_config', None) kilo_config = getattr(provider_config, 'kilo_config', None)
credentials_file = None credentials_file = None
api_base = getattr(provider_config, 'endpoint', 'https://api.kilo.ai') api_base = getattr(provider_config, 'endpoint', 'https://api.kilo.ai')
...@@ -1320,7 +1322,10 @@ async def dashboard_analytics( ...@@ -1320,7 +1322,10 @@ async def dashboard_analytics(
end = to_datetime or datetime.now() end = to_datetime or datetime.now()
date_range_usage = analytics.get_token_usage_by_date_range(provider_filter, start, end) date_range_usage = analytics.get_token_usage_by_date_range(provider_filter, start, end)
return templates.TemplateResponse("dashboard/analytics.html", { return templates.TemplateResponse(
request=request,
name="dashboard/analytics.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"provider_stats": provider_stats, "provider_stats": provider_stats,
...@@ -1342,12 +1347,32 @@ async def dashboard_analytics( ...@@ -1342,12 +1347,32 @@ async def dashboard_analytics(
"selected_rotation": rotation_filter, "selected_rotation": rotation_filter,
"selected_autoselect": autoselect_filter, "selected_autoselect": autoselect_filter,
"selected_user": user_filter "selected_user": user_filter
}) }
)
@app.get("/dashboard/login", response_class=HTMLResponse) @app.get("/dashboard/login", response_class=HTMLResponse)
async def dashboard_login_page(request: Request): async def dashboard_login_page(request: Request):
"""Show dashboard login page""" """Show dashboard login page"""
return templates.TemplateResponse("dashboard/login.html", {"request": request}) import logging
from jinja2 import Environment, FileSystemLoader, DictLoader
logger = logging.getLogger(__name__)
try:
# Create a completely fresh Jinja2 environment to avoid any caching issues
env = Environment(loader=FileSystemLoader("templates"), auto_reload=False)
# Add the required globals
env.globals['url_for'] = url_for
env.globals['get_base_url'] = get_base_url
# Get and render template
template = env.get_template("dashboard/login.html")
html_content = template.render(request=request)
return HTMLResponse(content=html_content)
except Exception as e:
logger.error(f"Error rendering login page: {e}", exc_info=True)
raise
@app.post("/dashboard/login") @app.post("/dashboard/login")
async def dashboard_login(request: Request, username: str = Form(...), password: str = Form(...)): async def dashboard_login(request: Request, username: str = Form(...), password: str = Form(...)):
...@@ -1381,7 +1406,11 @@ async def dashboard_login(request: Request, username: str = Form(...), password: ...@@ -1381,7 +1406,11 @@ async def dashboard_login(request: Request, username: str = Form(...), password:
request.session['user_id'] = None # Config admin has no user_id request.session['user_id'] = None # Config admin has no user_id
return RedirectResponse(url=url_for(request, "/dashboard"), status_code=303) return RedirectResponse(url=url_for(request, "/dashboard"), status_code=303)
return templates.TemplateResponse("dashboard/login.html", {"request": request, "error": "Invalid credentials"}) return templates.TemplateResponse(
request=request,
name="dashboard/login.html",
context={"request": request, "error": "Invalid credentials"}
)
@app.get("/dashboard/logout") @app.get("/dashboard/logout")
async def dashboard_logout(request: Request): async def dashboard_logout(request: Request):
...@@ -1407,20 +1436,27 @@ def require_admin(request: Request): ...@@ -1407,20 +1436,27 @@ def require_admin(request: Request):
@app.get("/dashboard", response_class=HTMLResponse) @app.get("/dashboard", response_class=HTMLResponse)
async def dashboard_index(request: Request): async def dashboard_index(request: Request):
"""Dashboard overview page""" """Dashboard overview page"""
# Clear template cache to prevent unhashable dict errors
templates.env.cache.clear()
auth_check = require_dashboard_auth(request) auth_check = require_dashboard_auth(request)
if auth_check: if auth_check:
return auth_check return auth_check
if request.session.get('role') == 'admin': if request.session.get('role') == 'admin':
# Admin dashboard # Admin dashboard
return templates.TemplateResponse("dashboard/index.html", { return templates.TemplateResponse(
request=request,
name="dashboard/index.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"providers_count": len(config.providers) if config else 0, "providers_count": len(config.providers) if config else 0,
"rotations_count": len(config.rotations) if config else 0, "rotations_count": len(config.rotations) if config else 0,
"autoselect_count": len(config.autoselect) if config else 0, "autoselect_count": len(config.autoselect) if config else 0,
"server_config": server_config or {} "server_config": server_config or {}
}) }
)
else: else:
# User dashboard - show user stats # User dashboard - show user stats
from aisbf.database import get_database from aisbf.database import get_database
...@@ -1459,7 +1495,10 @@ async def dashboard_index(request: Request): ...@@ -1459,7 +1495,10 @@ async def dashboard_index(request: Request):
autoselects_count = 0 autoselects_count = 0
recent_activity = [] recent_activity = []
return templates.TemplateResponse("dashboard/user_index.html", { return templates.TemplateResponse(
request=request,
name="dashboard/user_index.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"usage_stats": usage_stats, "usage_stats": usage_stats,
...@@ -1467,7 +1506,8 @@ async def dashboard_index(request: Request): ...@@ -1467,7 +1506,8 @@ async def dashboard_index(request: Request):
"rotations_count": rotations_count, "rotations_count": rotations_count,
"autoselects_count": autoselects_count, "autoselects_count": autoselects_count,
"recent_activity": recent_activity "recent_activity": recent_activity
}) }
)
@app.get("/dashboard/providers", response_class=HTMLResponse) @app.get("/dashboard/providers", response_class=HTMLResponse)
async def dashboard_providers(request: Request): async def dashboard_providers(request: Request):
...@@ -1494,12 +1534,16 @@ async def dashboard_providers(request: Request): ...@@ -1494,12 +1534,16 @@ async def dashboard_providers(request: Request):
# Check for success parameter # Check for success parameter
success = request.query_params.get('success') success = request.query_params.get('success')
return templates.TemplateResponse("dashboard/providers.html", { return templates.TemplateResponse(
request=request,
name="dashboard/providers.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"providers_json": json.dumps(providers_data), "providers_json": json.dumps(providers_data),
"success": "Configuration saved successfully! Restart server for changes to take effect." if success else None "success": "Configuration saved successfully! Restart server for changes to take effect." if success else None
}) }
)
async def _auto_detect_provider_models(provider_key: str, provider: dict) -> list: async def _auto_detect_provider_models(provider_key: str, provider: dict) -> list:
""" """
...@@ -1543,7 +1587,7 @@ async def _auto_detect_provider_models(provider_key: str, provider: dict) -> lis ...@@ -1543,7 +1587,7 @@ async def _auto_detect_provider_models(provider_key: str, provider: dict) -> lis
# For Kilo providers, try to get OAuth2 token # For Kilo providers, try to get OAuth2 token
if is_kilo_provider: if is_kilo_provider:
from aisbf.kilo_oauth2 import KiloOAuth2 from aisbf.auth.kilo import KiloOAuth2
kilo_config = provider.get('kilo_config', {}) kilo_config = provider.get('kilo_config', {})
credentials_file = kilo_config.get('credentials_file', '~/.kilo_credentials.json') credentials_file = kilo_config.get('credentials_file', '~/.kilo_credentials.json')
oauth2 = KiloOAuth2(credentials_file=credentials_file, api_base=endpoint) oauth2 = KiloOAuth2(credentials_file=credentials_file, api_base=endpoint)
...@@ -1690,12 +1734,16 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)): ...@@ -1690,12 +1734,16 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)):
if auto_detected_count > 0: if auto_detected_count > 0:
success_msg = f"Configuration saved successfully! Auto-detected models for {auto_detected_count} provider(s). Restart server for changes to take effect." success_msg = f"Configuration saved successfully! Auto-detected models for {auto_detected_count} provider(s). Restart server for changes to take effect."
return templates.TemplateResponse("dashboard/providers.html", { return templates.TemplateResponse(
request=request,
name="dashboard/providers.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"providers_json": json.dumps(providers_data), "providers_json": json.dumps(providers_data),
"success": success_msg "success": success_msg
}) }
)
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
# Reload current config on error # Reload current config on error
config_path = Path.home() / '.aisbf' / 'providers.json' config_path = Path.home() / '.aisbf' / 'providers.json'
...@@ -1710,12 +1758,16 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)): ...@@ -1710,12 +1758,16 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)):
else: else:
providers_data = {k: v for k, v in full_config.items() if k != 'condensation'} providers_data = {k: v for k, v in full_config.items() if k != 'condensation'}
return templates.TemplateResponse("dashboard/providers.html", { return templates.TemplateResponse(
request=request,
name="dashboard/providers.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"providers_json": json.dumps(providers_data), "providers_json": json.dumps(providers_data),
"error": f"Invalid JSON: {str(e)}" "error": f"Invalid JSON: {str(e)}"
}) }
)
@app.post("/dashboard/providers/get-models") @app.post("/dashboard/providers/get-models")
async def dashboard_providers_get_models(request: Request): async def dashboard_providers_get_models(request: Request):
...@@ -1814,13 +1866,17 @@ async def dashboard_rotations(request: Request): ...@@ -1814,13 +1866,17 @@ async def dashboard_rotations(request: Request):
# Check for success parameter # Check for success parameter
success = request.query_params.get('success') success = request.query_params.get('success')
return templates.TemplateResponse("dashboard/rotations.html", { return templates.TemplateResponse(
request=request,
name="dashboard/rotations.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"rotations_json": json.dumps(rotations_data), "rotations_json": json.dumps(rotations_data),
"available_providers": json.dumps(available_providers), "available_providers": json.dumps(available_providers),
"success": "Configuration saved successfully! Restart server for changes to take effect." if success else None "success": "Configuration saved successfully! Restart server for changes to take effect." if success else None
}) }
)
@app.post("/dashboard/rotations") @app.post("/dashboard/rotations")
async def dashboard_rotations_save(request: Request, config: str = Form(...)): async def dashboard_rotations_save(request: Request, config: str = Form(...)):
...@@ -1850,13 +1906,17 @@ async def dashboard_rotations_save(request: Request, config: str = Form(...)): ...@@ -1850,13 +1906,17 @@ async def dashboard_rotations_save(request: Request, config: str = Form(...)):
available_providers = list(config.providers.keys()) if config else [] available_providers = list(config.providers.keys()) if config else []
return templates.TemplateResponse("dashboard/rotations.html", { return templates.TemplateResponse(
request=request,
name="dashboard/rotations.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"rotations_json": json.dumps(rotations_data), "rotations_json": json.dumps(rotations_data),
"available_providers": json.dumps(available_providers), "available_providers": json.dumps(available_providers),
"success": "Configuration saved successfully! Restart server for changes to take effect." "success": "Configuration saved successfully! Restart server for changes to take effect."
}) }
)
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
# Reload current config on error # Reload current config on error
config_path = Path.home() / '.aisbf' / 'rotations.json' config_path = Path.home() / '.aisbf' / 'rotations.json'
...@@ -1867,13 +1927,17 @@ async def dashboard_rotations_save(request: Request, config: str = Form(...)): ...@@ -1867,13 +1927,17 @@ async def dashboard_rotations_save(request: Request, config: str = Form(...)):
available_providers = list(config.providers.keys()) if config else [] available_providers = list(config.providers.keys()) if config else []
return templates.TemplateResponse("dashboard/rotations.html", { return templates.TemplateResponse(
request=request,
name="dashboard/rotations.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"rotations_json": json.dumps(rotations_data), "rotations_json": json.dumps(rotations_data),
"available_providers": json.dumps(available_providers), "available_providers": json.dumps(available_providers),
"error": f"Invalid JSON: {str(e)}" "error": f"Invalid JSON: {str(e)}"
}) }
)
@app.get("/dashboard/autoselect", response_class=HTMLResponse) @app.get("/dashboard/autoselect", response_class=HTMLResponse)
async def dashboard_autoselect(request: Request): async def dashboard_autoselect(request: Request):
...@@ -1926,14 +1990,18 @@ async def dashboard_autoselect(request: Request): ...@@ -1926,14 +1990,18 @@ async def dashboard_autoselect(request: Request):
# Check for success parameter # Check for success parameter
success = request.query_params.get('success') success = request.query_params.get('success')
return templates.TemplateResponse("dashboard/autoselect.html", { return templates.TemplateResponse(
request=request,
name="dashboard/autoselect.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"autoselect_json": json.dumps(autoselect_data), "autoselect_json": json.dumps(autoselect_data),
"available_rotations": json.dumps(available_rotations), "available_rotations": json.dumps(available_rotations),
"available_models": json.dumps(available_models), "available_models": json.dumps(available_models),
"success": "Configuration saved successfully! Restart server for changes to take effect." if success else None "success": "Configuration saved successfully! Restart server for changes to take effect." if success else None
}) }
)
@app.post("/dashboard/autoselect") @app.post("/dashboard/autoselect")
async def dashboard_autoselect_save(request: Request, config: str = Form(...)): async def dashboard_autoselect_save(request: Request, config: str = Form(...)):
...@@ -1951,13 +2019,17 @@ async def dashboard_autoselect_save(request: Request, config: str = Form(...)): ...@@ -1951,13 +2019,17 @@ async def dashboard_autoselect_save(request: Request, config: str = Form(...)):
available_rotations = list(config.rotations.keys()) if config else [] available_rotations = list(config.rotations.keys()) if config else []
return templates.TemplateResponse("dashboard/autoselect.html", { return templates.TemplateResponse(
request=request,
name="dashboard/autoselect.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"autoselect_json": json.dumps(autoselect_data), "autoselect_json": json.dumps(autoselect_data),
"available_rotations": json.dumps(available_rotations), "available_rotations": json.dumps(available_rotations),
"success": "Configuration saved successfully! Restart server for changes to take effect." "success": "Configuration saved successfully! Restart server for changes to take effect."
}) }
)
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
# Reload current config on error # Reload current config on error
config_path = Path.home() / '.aisbf' / 'autoselect.json' config_path = Path.home() / '.aisbf' / 'autoselect.json'
...@@ -1968,13 +2040,17 @@ async def dashboard_autoselect_save(request: Request, config: str = Form(...)): ...@@ -1968,13 +2040,17 @@ async def dashboard_autoselect_save(request: Request, config: str = Form(...)):
available_rotations = list(config.rotations.keys()) if config else [] available_rotations = list(config.rotations.keys()) if config else []
return templates.TemplateResponse("dashboard/autoselect.html", { return templates.TemplateResponse(
request=request,
name="dashboard/autoselect.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"autoselect_json": json.dumps(autoselect_data), "autoselect_json": json.dumps(autoselect_data),
"available_rotations": json.dumps(available_rotations), "available_rotations": json.dumps(available_rotations),
"error": f"Invalid JSON: {str(e)}" "error": f"Invalid JSON: {str(e)}"
}) }
)
@app.get("/dashboard/prompts", response_class=HTMLResponse) @app.get("/dashboard/prompts", response_class=HTMLResponse)
async def dashboard_prompts(request: Request): async def dashboard_prompts(request: Request):
...@@ -2043,13 +2119,17 @@ async def dashboard_prompts(request: Request): ...@@ -2043,13 +2119,17 @@ async def dashboard_prompts(request: Request):
# Check for success parameter # Check for success parameter
success = request.query_params.get('success') success = request.query_params.get('success')
return templates.TemplateResponse("dashboard/prompts.html", { return templates.TemplateResponse(
request=request,
name="dashboard/prompts.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"prompts": prompt_files, "prompts": prompt_files,
"prompts_data": json.dumps(prompts_data), "prompts_data": json.dumps(prompts_data),
"success": "Prompt saved successfully!" if success else None "success": "Prompt saved successfully!" if success else None
}) }
)
@app.post("/dashboard/prompts") @app.post("/dashboard/prompts")
async def dashboard_prompts_save(request: Request, prompt_key: str = Form(...), prompt_content: str = Form(...)): async def dashboard_prompts_save(request: Request, prompt_key: str = Form(...), prompt_content: str = Form(...)):
...@@ -2066,13 +2146,17 @@ async def dashboard_prompts_save(request: Request, prompt_key: str = Form(...), ...@@ -2066,13 +2146,17 @@ async def dashboard_prompts_save(request: Request, prompt_key: str = Form(...),
} }
if prompt_key not in prompt_map: if prompt_key not in prompt_map:
return templates.TemplateResponse("dashboard/prompts.html", { return templates.TemplateResponse(
request=request,
name="dashboard/prompts.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"prompts": [], "prompts": [],
"prompts_data": "[]", "prompts_data": "[]",
"error": "Invalid prompt key" "error": "Invalid prompt key"
}) }
)
filename = prompt_map[prompt_key] filename = prompt_map[prompt_key]
config_path = Path.home() / '.aisbf' / filename config_path = Path.home() / '.aisbf' / filename
...@@ -2152,11 +2236,15 @@ async def dashboard_settings(request: Request): ...@@ -2152,11 +2236,15 @@ async def dashboard_settings(request: Request):
'fullconfig_tokens': [] 'fullconfig_tokens': []
} }
return templates.TemplateResponse("dashboard/settings.html", { return templates.TemplateResponse(
request=request,
name="dashboard/settings.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"config": aisbf_config "config": aisbf_config
}) }
)
@app.post("/dashboard/settings") @app.post("/dashboard/settings")
async def dashboard_settings_save( async def dashboard_settings_save(
...@@ -2269,19 +2357,27 @@ async def dashboard_settings_save( ...@@ -2269,19 +2357,27 @@ async def dashboard_settings_save(
with open(config_path, 'w') as f: with open(config_path, 'w') as f:
json.dump(aisbf_config, f, indent=2) json.dump(aisbf_config, f, indent=2)
return templates.TemplateResponse("dashboard/settings.html", { return templates.TemplateResponse(
request=request,
name="dashboard/settings.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"config": aisbf_config, "config": aisbf_config,
"success": "Settings saved successfully! Restart server for changes to take effect." "success": "Settings saved successfully! Restart server for changes to take effect."
}) }
)
return templates.TemplateResponse("dashboard/settings.html", { return templates.TemplateResponse(
request=request,
name="dashboard/settings.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"config": aisbf_config, "config": aisbf_config,
"success": "Settings saved successfully! Restart server for changes to take effect." "success": "Settings saved successfully! Restart server for changes to take effect."
}) }
)
# Admin user management routes # Admin user management routes
@app.get("/dashboard/users", response_class=HTMLResponse) @app.get("/dashboard/users", response_class=HTMLResponse)
...@@ -2297,11 +2393,15 @@ async def dashboard_users(request: Request): ...@@ -2297,11 +2393,15 @@ async def dashboard_users(request: Request):
# Get all users # Get all users
users = db.get_users() users = db.get_users()
return templates.TemplateResponse("dashboard/users.html", { return templates.TemplateResponse(
request=request,
name="dashboard/users.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"users": users "users": users
}) }
)
@app.post("/dashboard/users/add") @app.post("/dashboard/users/add")
async def dashboard_users_add(request: Request, username: str = Form(...), password: str = Form(...), role: str = Form("user")): async def dashboard_users_add(request: Request, username: str = Form(...), password: str = Form(...), role: str = Form("user")):
...@@ -2323,12 +2423,16 @@ async def dashboard_users_add(request: Request, username: str = Form(...), passw ...@@ -2323,12 +2423,16 @@ async def dashboard_users_add(request: Request, username: str = Form(...), passw
return RedirectResponse(url=url_for(request, "/dashboard/users"), status_code=303) return RedirectResponse(url=url_for(request, "/dashboard/users"), status_code=303)
except Exception as e: except Exception as e:
users = db.get_users() users = db.get_users()
return templates.TemplateResponse("dashboard/users.html", { return templates.TemplateResponse(
request=request,
name="dashboard/users.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"users": users, "users": users,
"error": f"Failed to create user: {str(e)}" "error": f"Failed to create user: {str(e)}"
}) }
)
@app.post("/dashboard/users/{user_id}/edit") @app.post("/dashboard/users/{user_id}/edit")
async def dashboard_users_edit(request: Request, user_id: int, username: str = Form(...), password: str = Form(""), role: str = Form("user"), is_active: bool = Form(True)): async def dashboard_users_edit(request: Request, user_id: int, username: str = Form(...), password: str = Form(""), role: str = Form("user"), is_active: bool = Form(True)):
...@@ -2350,12 +2454,16 @@ async def dashboard_users_edit(request: Request, user_id: int, username: str = F ...@@ -2350,12 +2454,16 @@ async def dashboard_users_edit(request: Request, user_id: int, username: str = F
return RedirectResponse(url=url_for(request, "/dashboard/users"), status_code=303) return RedirectResponse(url=url_for(request, "/dashboard/users"), status_code=303)
except Exception as e: except Exception as e:
users = db.get_users() users = db.get_users()
return templates.TemplateResponse("dashboard/users.html", { return templates.TemplateResponse(
request=request,
name="dashboard/users.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"users": users, "users": users,
"error": f"Failed to update user: {str(e)}" "error": f"Failed to update user: {str(e)}"
}) }
)
@app.post("/dashboard/users/{user_id}/toggle") @app.post("/dashboard/users/{user_id}/toggle")
async def dashboard_users_toggle(request: Request, user_id: int): async def dashboard_users_toggle(request: Request, user_id: int):
...@@ -2435,12 +2543,16 @@ async def dashboard_user_providers(request: Request): ...@@ -2435,12 +2543,16 @@ async def dashboard_user_providers(request: Request):
# Get user-specific providers # Get user-specific providers
user_providers = db.get_user_providers(user_id) user_providers = db.get_user_providers(user_id)
return templates.TemplateResponse("dashboard/user_providers.html", { return templates.TemplateResponse(
request=request,
name="dashboard/user_providers.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"user_providers_json": json.dumps(user_providers), "user_providers_json": json.dumps(user_providers),
"user_id": user_id "user_id": user_id
}) }
)
@app.post("/dashboard/user/providers") @app.post("/dashboard/user/providers")
async def dashboard_user_providers_save(request: Request, provider_name: str = Form(...), provider_config: str = Form(...)): async def dashboard_user_providers_save(request: Request, provider_name: str = Form(...), provider_config: str = Form(...)):
...@@ -2467,13 +2579,17 @@ async def dashboard_user_providers_save(request: Request, provider_name: str = F ...@@ -2467,13 +2579,17 @@ async def dashboard_user_providers_save(request: Request, provider_name: str = F
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
# Reload current providers on error # Reload current providers on error
user_providers = db.get_user_providers(user_id) user_providers = db.get_user_providers(user_id)
return templates.TemplateResponse("dashboard/user_providers.html", { return templates.TemplateResponse(
request=request,
name="dashboard/user_providers.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"user_providers_json": json.dumps(user_providers), "user_providers_json": json.dumps(user_providers),
"user_id": user_id, "user_id": user_id,
"error": f"Invalid JSON: {str(e)}" "error": f"Invalid JSON: {str(e)}"
}) }
)
@app.delete("/dashboard/user/providers/{provider_name}") @app.delete("/dashboard/user/providers/{provider_name}")
async def dashboard_user_providers_delete(request: Request, provider_name: str): async def dashboard_user_providers_delete(request: Request, provider_name: str):
...@@ -2819,12 +2935,16 @@ async def dashboard_user_rotations(request: Request): ...@@ -2819,12 +2935,16 @@ async def dashboard_user_rotations(request: Request):
# Get user-specific rotations # Get user-specific rotations
user_rotations = db.get_user_rotations(user_id) user_rotations = db.get_user_rotations(user_id)
return templates.TemplateResponse("dashboard/user_rotations.html", { return templates.TemplateResponse(
request=request,
name="dashboard/user_rotations.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"user_rotations_json": json.dumps(user_rotations), "user_rotations_json": json.dumps(user_rotations),
"user_id": user_id "user_id": user_id
}) }
)
@app.post("/dashboard/user/rotations") @app.post("/dashboard/user/rotations")
async def dashboard_user_rotations_save(request: Request, rotation_name: str = Form(...), rotation_config: str = Form(...)): async def dashboard_user_rotations_save(request: Request, rotation_name: str = Form(...), rotation_config: str = Form(...)):
...@@ -2851,13 +2971,17 @@ async def dashboard_user_rotations_save(request: Request, rotation_name: str = F ...@@ -2851,13 +2971,17 @@ async def dashboard_user_rotations_save(request: Request, rotation_name: str = F
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
# Reload current rotations on error # Reload current rotations on error
user_rotations = db.get_user_rotations(user_id) user_rotations = db.get_user_rotations(user_id)
return templates.TemplateResponse("dashboard/user_rotations.html", { return templates.TemplateResponse(
request=request,
name="dashboard/user_rotations.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"user_rotations_json": json.dumps(user_rotations), "user_rotations_json": json.dumps(user_rotations),
"user_id": user_id, "user_id": user_id,
"error": f"Invalid JSON: {str(e)}" "error": f"Invalid JSON: {str(e)}"
}) }
)
@app.delete("/dashboard/user/rotations/{rotation_name}") @app.delete("/dashboard/user/rotations/{rotation_name}")
async def dashboard_user_rotations_delete(request: Request, rotation_name: str): async def dashboard_user_rotations_delete(request: Request, rotation_name: str):
...@@ -2897,12 +3021,16 @@ async def dashboard_user_autoselects(request: Request): ...@@ -2897,12 +3021,16 @@ async def dashboard_user_autoselects(request: Request):
# Get user-specific autoselects # Get user-specific autoselects
user_autoselects = db.get_user_autoselects(user_id) user_autoselects = db.get_user_autoselects(user_id)
return templates.TemplateResponse("dashboard/user_autoselects.html", { return templates.TemplateResponse(
request=request,
name="dashboard/user_autoselects.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"user_autoselects_json": json.dumps(user_autoselects), "user_autoselects_json": json.dumps(user_autoselects),
"user_id": user_id "user_id": user_id
}) }
)
@app.post("/dashboard/user/autoselects") @app.post("/dashboard/user/autoselects")
async def dashboard_user_autoselects_save(request: Request, autoselect_name: str = Form(...), autoselect_config: str = Form(...)): async def dashboard_user_autoselects_save(request: Request, autoselect_name: str = Form(...), autoselect_config: str = Form(...)):
...@@ -2929,13 +3057,17 @@ async def dashboard_user_autoselects_save(request: Request, autoselect_name: str ...@@ -2929,13 +3057,17 @@ async def dashboard_user_autoselects_save(request: Request, autoselect_name: str
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
# Reload current autoselects on error # Reload current autoselects on error
user_autoselects = db.get_user_autoselects(user_id) user_autoselects = db.get_user_autoselects(user_id)
return templates.TemplateResponse("dashboard/user_autoselects.html", { return templates.TemplateResponse(
request=request,
name="dashboard/user_autoselects.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"user_autoselects_json": json.dumps(user_autoselects), "user_autoselects_json": json.dumps(user_autoselects),
"user_id": user_id, "user_id": user_id,
"error": f"Invalid JSON: {str(e)}" "error": f"Invalid JSON: {str(e)}"
}) }
)
@app.delete("/dashboard/user/autoselects/{autoselect_name}") @app.delete("/dashboard/user/autoselects/{autoselect_name}")
async def dashboard_user_autoselects_delete(request: Request, autoselect_name: str): async def dashboard_user_autoselects_delete(request: Request, autoselect_name: str):
...@@ -2975,12 +3107,16 @@ async def dashboard_user_tokens(request: Request): ...@@ -2975,12 +3107,16 @@ async def dashboard_user_tokens(request: Request):
# Get user API tokens # Get user API tokens
user_tokens = db.get_user_api_tokens(user_id) user_tokens = db.get_user_api_tokens(user_id)
return templates.TemplateResponse("dashboard/user_tokens.html", { return templates.TemplateResponse(
request=request,
name="dashboard/user_tokens.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"user_tokens": user_tokens, "user_tokens": user_tokens,
"user_id": user_id "user_id": user_id
}) }
)
@app.post("/dashboard/user/tokens") @app.post("/dashboard/user/tokens")
async def dashboard_user_tokens_create(request: Request, description: str = Form("")): async def dashboard_user_tokens_create(request: Request, description: str = Form("")):
...@@ -3088,10 +3224,14 @@ async def dashboard_rate_limits(request: Request): ...@@ -3088,10 +3224,14 @@ async def dashboard_rate_limits(request: Request):
if auth_check: if auth_check:
return auth_check return auth_check
return templates.TemplateResponse("dashboard/rate_limits.html", { return templates.TemplateResponse(
request=request,
name="dashboard/rate_limits.html",
context={
"request": request, "request": request,
"session": request.session "session": request.session
}) }
)
@app.get("/dashboard/rate-limits/data") @app.get("/dashboard/rate-limits/data")
async def dashboard_rate_limits_data(request: Request): async def dashboard_rate_limits_data(request: Request):
...@@ -3184,12 +3324,16 @@ async def dashboard_docs(request: Request): ...@@ -3184,12 +3324,16 @@ async def dashboard_docs(request: Request):
else: else:
html_content = "<p>Documentation file not found.</p>" html_content = "<p>Documentation file not found.</p>"
return templates.TemplateResponse("dashboard/docs.html", { return templates.TemplateResponse(
request=request,
name="dashboard/docs.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"content": html_content, "content": html_content,
"title": "Documentation" "title": "Documentation"
}) }
)
@app.get("/dashboard/about", response_class=HTMLResponse) @app.get("/dashboard/about", response_class=HTMLResponse)
async def dashboard_about(request: Request): async def dashboard_about(request: Request):
...@@ -3223,12 +3367,16 @@ async def dashboard_about(request: Request): ...@@ -3223,12 +3367,16 @@ async def dashboard_about(request: Request):
else: else:
html_content = "<p>README file not found.</p>" html_content = "<p>README file not found.</p>"
return templates.TemplateResponse("dashboard/docs.html", { return templates.TemplateResponse(
request=request,
name="dashboard/docs.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"content": html_content, "content": html_content,
"title": "About" "title": "About"
}) }
)
@app.get("/dashboard/license", response_class=HTMLResponse) @app.get("/dashboard/license", response_class=HTMLResponse)
async def dashboard_license(request: Request): async def dashboard_license(request: Request):
...@@ -3259,12 +3407,16 @@ async def dashboard_license(request: Request): ...@@ -3259,12 +3407,16 @@ async def dashboard_license(request: Request):
else: else:
html_content = "<p>License file not found.</p>" html_content = "<p>License file not found.</p>"
return templates.TemplateResponse("dashboard/docs.html", { return templates.TemplateResponse(
request=request,
name="dashboard/docs.html",
context={
"request": request, "request": request,
"session": request.session, "session": request.session,
"content": html_content, "content": html_content,
"title": "License" "title": "License"
}) }
)
def parse_provider_from_model(model: str) -> tuple[str, str]: def parse_provider_from_model(model: str) -> tuple[str, str]:
""" """
...@@ -5909,7 +6061,7 @@ async def dashboard_claude_auth_start(request: Request): ...@@ -5909,7 +6061,7 @@ async def dashboard_claude_auth_start(request: Request):
) )
# Import ClaudeAuth # Import ClaudeAuth
from aisbf.claude_auth import ClaudeAuth from aisbf.auth.claude import ClaudeAuth
# Create auth instance # Create auth instance
auth = ClaudeAuth() auth = ClaudeAuth()
...@@ -6016,7 +6168,7 @@ async def dashboard_claude_auth_complete(request: Request): ...@@ -6016,7 +6168,7 @@ async def dashboard_claude_auth_complete(request: Request):
) )
# Import ClaudeAuth # Import ClaudeAuth
from aisbf.claude_auth import ClaudeAuth from aisbf.auth.claude import ClaudeAuth
# Create auth instance # Create auth instance
auth = ClaudeAuth() auth = ClaudeAuth()
...@@ -6110,7 +6262,7 @@ async def dashboard_claude_auth_status(request: Request): ...@@ -6110,7 +6262,7 @@ async def dashboard_claude_auth_status(request: Request):
) )
# Import ClaudeAuth # Import ClaudeAuth
from aisbf.claude_auth import ClaudeAuth from aisbf.auth.claude import ClaudeAuth
# Create auth instance # Create auth instance
auth = ClaudeAuth(credentials_file=credentials_file) auth = ClaudeAuth(credentials_file=credentials_file)
...@@ -6169,7 +6321,7 @@ async def dashboard_kilo_auth_start(request: Request): ...@@ -6169,7 +6321,7 @@ async def dashboard_kilo_auth_start(request: Request):
) )
# Import KiloOAuth2 # Import KiloOAuth2
from aisbf.kilo_oauth2 import KiloOAuth2 from aisbf.auth.kilo import KiloOAuth2
# Create auth instance # Create auth instance
auth = KiloOAuth2(credentials_file=credentials_file) auth = KiloOAuth2(credentials_file=credentials_file)
...@@ -6240,7 +6392,7 @@ async def dashboard_kilo_auth_poll(request: Request): ...@@ -6240,7 +6392,7 @@ async def dashboard_kilo_auth_poll(request: Request):
}) })
# Import KiloOAuth2 # Import KiloOAuth2
from aisbf.kilo_oauth2 import KiloOAuth2 from aisbf.auth.kilo import KiloOAuth2
# Create auth instance # Create auth instance
auth = KiloOAuth2(credentials_file=credentials_file) auth = KiloOAuth2(credentials_file=credentials_file)
...@@ -6345,7 +6497,7 @@ async def dashboard_kilo_auth_status(request: Request): ...@@ -6345,7 +6497,7 @@ async def dashboard_kilo_auth_status(request: Request):
) )
# Import KiloOAuth2 # Import KiloOAuth2
from aisbf.kilo_oauth2 import KiloOAuth2 from aisbf.auth.kilo import KiloOAuth2
# Create auth instance # Create auth instance
auth = KiloOAuth2(credentials_file=credentials_file) auth = KiloOAuth2(credentials_file=credentials_file)
...@@ -6395,7 +6547,7 @@ async def dashboard_kilo_auth_logout(request: Request): ...@@ -6395,7 +6547,7 @@ async def dashboard_kilo_auth_logout(request: Request):
) )
# Import KiloOAuth2 # Import KiloOAuth2
from aisbf.kilo_oauth2 import KiloOAuth2 from aisbf.auth.kilo import KiloOAuth2
# Create auth instance # Create auth instance
auth = KiloOAuth2(credentials_file=credentials_file) auth = KiloOAuth2(credentials_file=credentials_file)
......
...@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" ...@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "aisbf" name = "aisbf"
version = "0.9.5" version = "0.9.6"
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"
readme = "README.md" readme = "README.md"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
......
...@@ -49,7 +49,7 @@ class InstallCommand(_install): ...@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup( setup(
name="aisbf", name="aisbf",
version="0.9.5", version="0.9.6",
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",
......
...@@ -97,6 +97,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -97,6 +97,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
var currentIndex = 0; var currentIndex = 0;
document.addEventListener('keydown', function(e) { document.addEventListener('keydown', function(e) {
// Don't activate easter egg when typing in input fields
var activeElement = document.activeElement;
if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.tagName === 'SELECT')) {
return;
}
var now = Date.now(); var now = Date.now();
// Reset if too much time passed // Reset if too much time passed
......
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