diff --git a/apps/_dashboard/DASHBOARD_GUIDE.md b/apps/_dashboard/DASHBOARD_GUIDE.md new file mode 100644 index 000000000..866cbbcf5 --- /dev/null +++ b/apps/_dashboard/DASHBOARD_GUIDE.md @@ -0,0 +1,510 @@ +# PY4WEB Dashboard Guide + +## Overview + +The `_dashboard` app is a special administrative application that provides a web-based interface for managing all py4web applications. It has elevated privileges and follows a modular structure similar to standard py4web apps, but with intentional differences due to its administrative role. + +## Table of Contents + +- [Architecture](#architecture) +- [Key Differences from Standard Apps](#key-differences-from-standard-apps) +- [File Structure](#file-structure) +- [Theming](#theming) +- [Authentication & Authorization](#authentication--authorization) +- [API Endpoints](#api-endpoints) +- [Development Guide](#development-guide) +- [Security Considerations](#security-considerations) + +--- + +## Architecture + +The dashboard follows py4web's modular app structure but keeps most logic in a +single module because it is an administrative surface: + +``` +_dashboard/ +├── __init__.py # App initialization, fixtures, actions, APIs +├── settings.py # Configuration (MODE, FOLDER, etc.) +├── utils.py # File operations, git helpers, safe joins +├── diff2kryten.py # Git diff visualization +├── templates/ # YATL templates +├── static/ # Frontend assets (CSS, JS) +└── translations/ # i18n files +``` + +### Import Dependency Graph + +``` +__init__.py + ↓ ↓ ↓ +settings.py diff2kryten.py utils.py +``` + +**No circular imports** - The import chain is a clean directed acyclic graph (DAG). + +--- + +## Key Differences from Standard Apps + +The dashboard intentionally differs from typical py4web apps due to its administrative nature: + +### 1. **No Database Instance** + +Unlike standard apps that define `db = DAL(...)` in `common.py`, the dashboard has **no database of its own**. Instead, it: + +- Accesses other apps' databases dynamically via `Reloader.MODULES` +- Uses `error_logger.database_logger.db` for ticket management +- Wraps external databases with `make_safe()` to prevent forbidden method access + +### 2. **Custom Authentication** + +The dashboard uses a **custom `Logged` fixture** instead of the standard `Auth` framework: + +- **Why:** Dashboard manages all apps including those with Auth, so it can't depend on any app's Auth +- **How:** Password-based authentication using `PY4WEB_PASSWORD_FILE` environment variable +- **Where:** Defined in `__init__.py` + +### 3. **Minimal Fixtures** + +Standard apps typically have: `db`, `session`, `auth`, `cache`, `flash`, `T` + +Dashboard only has: `session`, `T` (translator), and `Logged` (custom auth) + +**Reason:** Dashboard is primarily an API/admin interface that doesn't need form helpers, caching, or flash messages. + +### 4. **Single-Module Actions** + +There is no `controllers.py` or `common.py`. All actions, fixtures, and helpers +live in `__init__.py`, while `utils.py` holds shared helpers. + +--- + +## File Structure + +### `__init__.py` - App Initialization, Fixtures, and Actions + +Registers the app with py4web and exposes all actions. + +**Key Elements:** +- Defines `session`, `T`, and `Logged` +- Declares all routes and API endpoints +- Uses `session_secured` and `authenticated` factories for access control + +--- + +### `settings.py` - Configuration + +Defines configuration from environment variables. + +**Environment Variables:** +- `PY4WEB_DASHBOARD_MODE`: `"demo"`, `"readonly"`, `"full"`, or `"none"` +- `PY4WEB_APPS_FOLDER`: Path to apps directory (required) +- `PY4WEB_APP_NAMES`: Comma-separated list of exposed app names (optional) +- `PY4WEB_PASSWORD_FILE`: Path to password file for authentication + +**Modes:** +- `none`: Dashboard disabled +- `demo`: Read-only with mock authentication +- `readonly`: View apps and settings but cannot modify +- `full`: Complete administrative access + +--- + +### `utils.py` - Shared Utilities + +```python +def make_safe(db): + """Wraps database field defaults/updates to prevent cross-app method access.""" + ... +``` + +Contains `make_safe()`, `safe_join()`, git helpers, and reference field helpers. + +**Purpose:** When dashboard accesses another app's database schema, field defaults like `default=lambda: get_user()` might reference functions that don't exist in dashboard's context. The `make_safe()` wrapper prevents crashes by wrapping these callables in error handlers. + +--- + +### Route Handlers + +All dashboard endpoints are defined in `__init__.py` using standard py4web action patterns: + +- **Public endpoints:** no authentication required +- **Authenticated endpoints:** use `@session_secured` or `@action.uses(Logged(session))` +- **API endpoints:** use `@catch_errors` to return `{"status": "success/error", ...}` +- **Template endpoints:** use `@action.uses("template.html", session, T)` + +### Setting Dashboard Password + +```bash +# Using CLI +py4web set_password + +# Or manually +python -c "from pydal.validators import CRYPT; print(CRYPT()('mypassword')[0])" > password.txt +export PY4WEB_PASSWORD_FILE=password.txt +``` + +--- + +## Theming + +The dashboard supports multiple themes using a CSS override pattern. This allows users to switch between themes dynamically without reloading the page, with theme preference persisted in browser localStorage. + +### Theme System Architecture + +**Folder Structure:** +``` +static/ +├── css/ +│ ├── future.css # Dark base stylesheet (main dashboard) +│ ├── no.css # Light base stylesheet (dbadmin pages) +│ └── ... +├── js/ +│ ├── theme-selector.js # Theme switching logic +│ └── ... +├── themes/ +│ ├── AlienDark/ +│ │ └── theme.css # Dark theme overrides +│ └── AlienLight/ +│ └── theme.css # Light theme overrides +└── ... +``` + +### How Theming Works + +1. **Base Stylesheets:** Dashboard loads a base stylesheet (`future.css` for main dashboard, `no.css` for dbadmin) that defines the default dark theme +2. **Theme CSS Variables:** Each theme defines CSS custom properties (variables) for colors and styling +3. **Dynamic Theme Loading:** JavaScript (`theme-selector.js`) dynamically loads theme CSS files by updating the `href` of a `` tag with id `dashboard-theme` +4. **Local Storage Persistence:** Selected theme is stored in browser localStorage under key `py4web-dashboard-theme` +5. **Auto-Apply:** On page load, theme-selector.js automatically applies the saved theme preference + +### Available Themes + +#### AlienDark +- **Description:** Dark theme with cyan accents +- **CSS Variables:** + - `--bg-primary: black` - Main background + - `--text-primary: #d1d1d1` - Primary text color + - `--accent: #33BFFF` - Accent color (cyan) + - `--accent-dark: #007a99` - Dark accent variant + - `--bg-secondary: #1a1a1a` - Secondary background + - `--border-color: #333` - Border color +- **File:** `static/themes/AlienDark/theme.css` + +#### AlienLight +- **Description:** Light theme with blue accents +- **CSS Variables:** + - `--bg-primary: white` - Main background + - `--text-primary: #333` - Primary text color + - `--accent: #0074d9` - Accent color (blue) + - `--accent-dark: #003d74` - Dark accent variant + - `--bg-secondary: #f5f5f5` - Secondary background + - `--border-color: #ddd` - Border color +- **File:** `static/themes/AlienLight/theme.css` + +### Using CSS Variables in Theme Files + +Each theme file uses CSS custom properties for consistent styling across components: + +```css +:root { + --bg-primary: black; + --text-primary: #d1d1d1; + --accent: #33BFFF; + /* ... other variables ... */ +} + +/* Override specific elements using variables */ +body { + background: var(--bg-primary); + color: var(--text-primary); +} + +button { + background: var(--accent); + color: var(--bg-primary); +} +``` + +### Theme Selector UI + +The theme selector is visible on the main dashboard (`index.html`): + +```html + +``` + +**Features:** +- Dropdown positioned in top-right corner of header +- Uses `data-theme-selector` attribute for synchronization +- Calls `setDashboardTheme()` function from `theme-selector.js` +- Multiple selectors on the same page stay synchronized + +### JavaScript Theme Switching (theme-selector.js) + +The theme selector module is now fully dynamic, detecting themes from the select element. See [theme-selector.js](static/js/theme-selector.js) for the implementation. + +**Key Features:** +- `getAvailableThemes()` - Dynamically reads themes from select element options (no hardcoded list) +- `getDefaultTheme()` - Returns "AlienDark" if available, otherwise the first theme alphabetically +- `applyTheme(theme)` - Loads theme CSS and persists selection in localStorage +- Auto-applies saved theme on page load, or default theme if none saved +- Multiple selectors on the same page stay synchronized +- Automatically detects new themes when they're added to the select element + +### Adding a New Theme + +To create a new theme (e.g., `MyCustomTheme`): + +1. **Create theme folder:** + ``` + static/themes/MyCustomTheme/ + ``` + +2. **Create `theme.css` with CSS variables:** + ```css + :root { + --bg-primary: #your-bg-color; + --text-primary: #your-text-color; + --accent: #your-accent-color; + --accent-dark: #your-accent-dark-color; + --bg-secondary: #your-secondary-bg; + --border-color: #your-border-color; + } + + /* Override styles using variables */ + body { + background: var(--bg-primary); + color: var(--text-primary); + } + /* ... more overrides ... */ + ``` + +3. **Add to theme selector in templates:** + The theme selector options are now **dynamically generated** from available theme folders in the backend. When you create a new theme folder, it's automatically listed in the select dropdown on both `index.html` and dbadmin pages via Python template iteration. + +### Best Practices for Theme Development + +1. **Use CSS Variables:** Always reference theme colors via custom properties, never hardcode colors +2. **Minimal Overrides:** Only include CSS rules that differ from the base stylesheet +3. **Test Both Locations:** Verify theme works on main dashboard (`index.html`) and dbadmin pages (`layout.html`) +4. **Check Full Viewport:** Ensure backgrounds extend to full viewport height, no black/white strips at bottom +5. **Text Contrast:** Verify text colors have sufficient contrast with background colors for accessibility +6. **Form Elements:** Ensure all form inputs (text, select, button, checkbox) are styled consistently + +--- + +## API Endpoints + +### App Management + +| Endpoint | Method | Auth | Description | +|----------|--------|------|-------------| +| `/version` | GET | No | Returns py4web version | +| `/index` | GET | No | Dashboard UI (login if needed) | +| `/login` | POST | No | Authenticate user | +| `/logout` | POST | Session | Clear session | +| `/apps` | GET | Yes | List all applications | +| `/reload` | GET | Yes | Reload all apps | +| `/reload/` | GET | Yes | Reload specific app | +| `/delete_app/` | POST | Yes | Delete app (archives first) | +| `/new_app` | POST | Yes | Create/update app from scaffold, web, or upload | + +### File Operations + +| Endpoint | Method | Auth | Mode | Description | +|----------|--------|------|------|-------------| +| `/walk/` | GET | Yes | Any | Get folder tree structure | +| `/load/` | GET | Yes | Any | Load text file content | +| `/load_bytes/` | GET | Yes | Any | Load binary file | +| `/save/` | POST | Yes | Full | Save file content | +| `/delete/` | POST | Yes | Full | Delete file | +| `/new_file//` | POST | Yes | Any | Create new file | +| `/packed/.zip` | GET | Yes | Any | Download app as ZIP | + +### Database Management + +| Endpoint | Method | Auth | Description | +|----------|--------|------|-------------| +| `/dbadmin///` | GET | Yes | CRUD interface for table | +| `/rest/` | GET/POST/PUT/DELETE | Yes | REST API for app databases | + +**DBAdmin Features:** + +- **Grid Interface:** Browse, search, sort, and paginate table records +- **Clickable Reference Fields:** Reference fields are automatically rendered as clickable links that navigate to the referenced record's table +- **Supported Field Types:** `id`, `string`, `integer`, `double`, `time`, `date`, `datetime`, `boolean`, `reference`, `big-reference` +- **Smart Display:** Reference fields show the referenced table's `_format` value (e.g., user name instead of ID) +- **Missing References:** Shows "#{id}(missing)" if referenced record doesn't exist + +### Error Tracking + +| Endpoint | Method | Auth | Description | +|----------|--------|------|-------------| +| `/tickets` | GET | Yes | List error tickets | +| `/tickets/search` | GET | Yes | Search tickets with Grid UI | +| `/ticket/` | GET | Yes | View ticket details | +| `/clear` | POST | Yes | Clear all tickets | + +### Development Tools + +| Endpoint | Method | Auth | Description | +|----------|--------|------|-------------| +| `/info` | GET | Yes | Python modules and versions | +| `/routes` | GET | Yes | All registered routes | +| `/translations/` | GET | Yes | Translation editor UI | +| `/api/translations/` | GET/POST | Yes | Get/update translations | +| `/api/translations//search` | GET | Yes | Find translatable strings | + +### Git Integration + +| Endpoint | Method | Auth | Description | +|----------|--------|------|-------------| +| `/gitlog/` | GET | Yes | View commit history | +| `/gitshow//` | GET | Yes | View commit diff | +| `/swapbranch/` | POST | Yes | Switch git branch | + +--- + +## Authentication & Authorization + +### Login Flow + +1. Client sends POST request to `/login` with password in JSON body +2. Server validates against encrypted password in `PY4WEB_PASSWORD_FILE` (skipped in `demo`) +3. On success, sets `session["user"] = {"id": 1}` +4. Subsequent requests include session cookie which `Logged` fixture validates + +### Setting Dashboard Password + +Use CLI: `py4web set_password` or manually encrypt password and save to file, then set `PY4WEB_PASSWORD_FILE` environment variable. + +### Working with External Databases + +```python +@action("dbadmin///") +@action.uses(Logged(session), "dbadmin.html") +def dbadmin_table(app_name, db_name, table_name): + # Get the app module + module = Reloader.MODULES.get(app_name) + if not module: + abort(404) + + # Get the database instance + db = getattr(module, db_name, None) + if not db: + abort(404) + + # CRITICAL: Wrap before using + make_safe(db) + + # Now safe to use + def make_grid(): + table = db[table_name] + + # Customize reference fields to be clickable + for field in table: + if field.type_name in ("reference", "big-reference", "list:reference"): + field.represent = make_admin_reference_represent(app_name, db_name, field) + + return Grid(table) + + grid = action.uses(db)(make_grid)() + return dict(grid=grid) +``` + +**Key Points:** + +- Always call `make_safe(db)` before accessing external databases +- Reference fields can be customized with `field.represent` functions +- Use `XML()` from `yatl.helpers` to return HTML content +- The Grid helper automatically uses field `represent` functions for rendering + +### Using Grid with External DB + +The `Grid` helper needs the database as a fixture. Use the pattern: + +```python +def make_grid(): + make_safe(db) + return Grid(db.table_name, ...) + +grid = action.uses(db)(make_grid)() +``` + +This ensures proper DB connection lifecycle. + +--- + +## Security Considerations + +### 1. **Environment-Based Authentication** + +The dashboard password is stored outside the codebase: + +```bash +export PY4WEB_PASSWORD_FILE=/secure/path/password.txt +``` + +**Never commit password files to version control.** + +### 2. **Mode Restrictions** + +Always guard dangerous operations: + +```python +if MODE != "full": + abort(403) # or raise HTTP(403) +``` + +### 3. **Path Traversal Protection** + +Use `safe_join()` from `utils.py`: + +```python +from .utils import safe_join + +path = safe_join(FOLDER, user_provided_path) or abort(400) +``` + +This prevents `../../../etc/passwd` attacks. + +### 4. **Database Wrapping** + +Always call `make_safe(db)` before accessing external databases to prevent forbidden method access. + +### 5. **Session Protection** + +All POST/PUT/DELETE actions require a valid session cookie and pass through `Logged`. + +### 6. **App Name Filtering** + +Limit visible apps via `PY4WEB_APP_NAMES` environment variable (comma-separated list). + +--- + +## Development Guide + +### Adding a New Endpoint + +1. Define action in `__init__.py` using `@action` +2. Use `@session_secured` or `@action.uses(Logged(session))` +3. Use `@catch_errors` for API endpoints +4. Guard dangerous operations with mode checks (`if MODE == "full"`) + +### Working with External Databases + +1. Get app module from `Reloader.MODULES.get(app_name)` +2. Get database instance from module +3. **Always call `make_safe(db)` before using** to prevent security issues +4. For Grid helper, wrap the grid creation in a function and use `action.uses(db)(make_grid)()` + +--- + + diff --git a/apps/_dashboard/__init__.py b/apps/_dashboard/__init__.py index a58ff8b08..f174c62f4 100644 --- a/apps/_dashboard/__init__.py +++ b/apps/_dashboard/__init__.py @@ -68,6 +68,21 @@ def wrapper(*args, **kwargs): return wrapper +def get_available_themes(): + """Get list of available themes by reading static/themes/ folder""" + themes_dir = os.path.join(settings.APP_FOLDER, "static", "themes") + try: + if os.path.isdir(themes_dir): + themes = sorted([ + d for d in os.listdir(themes_dir) + if os.path.isdir(os.path.join(themes_dir, d)) and not d.startswith('.') + ]) + return themes + except (OSError, IOError): + pass + return ["AlienDark", "AlienLight"] # Fallback + + session = Session() T = Translator(settings.T_FOLDER) authenticated = ActionFactory(Logged(session)) @@ -88,6 +103,7 @@ def index(): languages=dumps(getattr(T.local, "language", {})), mode=MODE, user_id=(session.get("user") or {}).get("id"), + themes=get_available_themes(), ) @action("login", method="POST") @@ -137,11 +153,12 @@ def make_grid(): ) grid = action.uses(db)(make_grid)() - return dict(table_name="py4web_error", grid=grid) + return dict(table_name="py4web_error", grid=grid, themes=get_available_themes()) @action("dbadmin///") @action.uses(Logged(session), "dbadmin.html") def dbadmin(app_name, db_name, table_name): + themes = get_available_themes() module = Reloader.MODULES.get(app_name) db = getattr(module, db_name) @@ -177,7 +194,7 @@ def make_grid(): return Grid(table, columns=columns) grid = action.uses(db)(make_grid)() - return dict(table_name=table_name, grid=grid) + return dict(app_name=app_name, table_name=table_name, grid=grid, themes=themes) @action("info") @session_secured diff --git a/apps/_dashboard/static/css/future.css b/apps/_dashboard/static/css/future.css index fd409f2ea..d7cbf1644 100644 --- a/apps/_dashboard/static/css/future.css +++ b/apps/_dashboard/static/css/future.css @@ -17,10 +17,11 @@ p { margin-bottom: 10px; } .logo { - padding: 18px; + padding: 0 0 0 8px; font-size: 64px; - position: absolute; - top: 0; + position: static; + line-height: 1; + display: inline-block; } .spinner-top { height: 80px; diff --git a/apps/_dashboard/static/favicon_green.ico b/apps/_dashboard/static/favicon_green.ico new file mode 100644 index 000000000..455bca52c Binary files /dev/null and b/apps/_dashboard/static/favicon_green.ico differ diff --git a/apps/_dashboard/static/images/widget-transparent.gif b/apps/_dashboard/static/images/widget-transparent.gif new file mode 100644 index 000000000..faa9d4381 Binary files /dev/null and b/apps/_dashboard/static/images/widget-transparent.gif differ diff --git a/apps/_dashboard/static/js/theme-selector.js b/apps/_dashboard/static/js/theme-selector.js new file mode 100644 index 000000000..8d71b03be --- /dev/null +++ b/apps/_dashboard/static/js/theme-selector.js @@ -0,0 +1,227 @@ +/** + * Dashboard Theme Selector Module + * + * Manages dynamic theme switching for the py4web dashboard with browser storage persistence. + * Themes are dynamically discovered from the select element, allowing new themes to be added + * without code changes. Theme selection is persisted using localStorage. + * + * Features: + * - Dynamic theme detection from DOM select element + * - localStorage persistence across sessions + * - Automatic synchronization of multiple theme selectors + * - Intelligent default theme selection (AlienDark if available, else first alphabetically) + * - Graceful fallback handling for storage errors + */ +(function () { + "use strict"; + + var STORAGE_KEY = "py4web-dashboard-theme"; + + /** + * Retrieves all available themes by reading options from the theme selector dropdown. + * This method dynamically discovers themes without requiring hardcoded lists. + * + * @returns {Array} Array of theme names (e.g., ["AlienDark", "AlienLight"]) + * Returns ["AlienDark"] as fallback if selector not found + */ + function getAvailableThemes() { + var selector = document.getElementById("dashboard-theme-select"); + if (selector) { + var themes = []; + for (var i = 0; i < selector.options.length; i += 1) { + themes.push(selector.options[i].value); + } + return themes.length > 0 ? themes : ["AlienDark", "AlienLight"]; + } + // Fallback to known themes if selector not found (useful during page load) + return ["AlienDark", "AlienLight"]; + } + + /** + * Determines the default theme to use when no theme is stored or invalid theme is requested. + * + * Selection logic: + * 1. If "AlienDark" is available, use it (preferred default) + * 2. Otherwise use the first available theme (alphabetically sorted) + * 3. Fallback to "AlienDark" if no themes available + * + * @returns {string} The default theme name + */ + function getDefaultTheme() { + var themes = getAvailableThemes(); + // Prefer AlienDark if available, otherwise use first alphabetically + if (themes.indexOf("AlienDark") !== -1) { + return "AlienDark"; + } + return themes.length > 0 ? themes[0] : "AlienDark"; + } + + /** + * Retrieves the previously stored theme from browser localStorage, if valid. + * Validates that the stored theme is still in the available themes list + * (handles case where a theme was removed after being selected). + * + * @returns {string|null} The stored theme name if valid and localStorage accessible, + * null otherwise (will fallback to default theme) + */ + function getStoredTheme() { + try { + var stored = localStorage.getItem(STORAGE_KEY); + var themes = getAvailableThemes(); + if (stored && themes.indexOf(stored) !== -1) { + return stored; + } + } catch (err) { + return null; + } + return null; + } + + /** + * Applies a theme by: + * 1. Validating the requested theme against available options + * 2. Updating the theme CSS link href + * 3. Setting data-theme attribute on document root + * 4. Updating the favicon based on theme + * 5. Updating app icons based on theme + * 6. Persisting the selection to localStorage + * 7. Syncing all theme selector dropdowns + * + * @param {string} theme - The theme name to apply + */ + function applyTheme(theme) { + var themes = getAvailableThemes(); + var defaultTheme = getDefaultTheme(); + var selected = themes.indexOf(theme) !== -1 ? theme : defaultTheme; + + // Load theme CSS file by updating the link element href + var link = document.getElementById("dashboard-theme"); + if (link) { + link.setAttribute("href", "themes/" + selected + "/theme.css"); + } + + // Update browser favicon based on theme + var favicon = document.querySelector("link[rel='shortcut icon']"); + if (favicon) { + if (selected === "AlienLight") { + favicon.setAttribute("href", "favicon_green.ico"); + } else { + favicon.setAttribute("href", "favicon.ico"); + } + } + + // Update the top-left spinner image for light theme + var spinner = document.querySelector("img.spinner-top"); + if (spinner) { + var originalSpinnerSrc = spinner.getAttribute("data-original-src"); + if (!originalSpinnerSrc) { + originalSpinnerSrc = spinner.getAttribute("src"); + spinner.setAttribute("data-original-src", originalSpinnerSrc); + } + if (selected === "AlienLight") { + spinner.setAttribute("src", "images/widget-transparent.gif"); + } else { + spinner.setAttribute("src", originalSpinnerSrc); + } + } + + // Update app icons based on theme (images with favicon.ico src) + var appIcons = document.querySelectorAll("img[src*='favicon']"); + for (var i = 0; i < appIcons.length; i += 1) { + var img = appIcons[i]; + var currentSrc = img.getAttribute("src"); + + // Skip if not a favicon icon + if (!currentSrc.includes("favicon")) continue; + + if (selected === "AlienLight") { + // Store original src if not already stored + if (!img.getAttribute("data-original-src")) { + img.setAttribute("data-original-src", currentSrc); + } + // Point all app icons to the dashboard's green favicon + img.setAttribute("src", "/_dashboard/static/favicon_green.ico"); + } else { + // Restore original favicon path + var originalSrc = img.getAttribute("data-original-src"); + if (originalSrc && originalSrc !== "/_dashboard/static/favicon_green.ico") { + // Use stored original + img.setAttribute("src", originalSrc); + } else if (currentSrc.includes("_dashboard") && currentSrc.includes("favicon_green")) { + // Currently pointing to green, extract the original app path + // For dashboard: /static/favicon.ico or /{app}/static/favicon.ico + var parts = document.location.pathname.split("/"); + if (parts[1] && parts[1] !== "_dashboard") { + img.setAttribute("src", "/" + parts[1] + "/static/favicon.ico"); + } else { + img.setAttribute("src", "/static/favicon.ico"); + } + } else if (currentSrc.includes("favicon_green")) { + // Try to reconstruct original from URL pattern + img.setAttribute("src", "/static/favicon.ico"); + } + } + } + + // Set data attribute for CSS selectors that might use it + document.documentElement.setAttribute("data-theme", selected); + + // Persist theme selection to localStorage + try { + localStorage.setItem(STORAGE_KEY, selected); + } catch (err) { + // Ignore storage errors (private browsing, full storage, etc.) + } + + // Update all theme selector dropdowns to reflect current theme + syncSelectors(selected); + } + + /** + * Synchronizes all theme selector dropdowns on the page to the same value. + * Allows multiple theme selectors (e.g., on different pages) to stay in sync. + * + * @param {string} theme - The theme value to set on all selectors + */ + function syncSelectors(theme) { + var selectors = document.querySelectorAll("[data-theme-selector]"); + for (var i = 0; i < selectors.length; i += 1) { + selectors[i].value = theme; + } + } + + /** + * Initializes the theme system on page load. + * Stores original favicon src values before applying any theme. + * Loads the previously saved theme, or applies the default if none saved. + */ + function init() { + // Store all original favicon srcs before applying theme + var appIcons = document.querySelectorAll("img[src*='favicon']"); + for (var i = 0; i < appIcons.length; i += 1) { + var img = appIcons[i]; + var currentSrc = img.getAttribute("src"); + if (currentSrc && !img.getAttribute("data-original-src")) { + img.setAttribute("data-original-src", currentSrc); + } + } + + var initial = getStoredTheme() || getDefaultTheme(); + applyTheme(initial); + } + + /** + * Public API: Exposed globally to allow HTML onclick handlers and external code + * to trigger theme changes. Called by the theme selector dropdown's onchange event. + * + * Usage: setDashboardTheme("AlienDark") + */ + window.setDashboardTheme = applyTheme; + + // Initialize theme on page load + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})(); diff --git a/apps/_dashboard/static/themes/AlienDark/theme.css b/apps/_dashboard/static/themes/AlienDark/theme.css new file mode 100644 index 000000000..e60327a5b --- /dev/null +++ b/apps/_dashboard/static/themes/AlienDark/theme.css @@ -0,0 +1,169 @@ +/** + * AlienDark Theme - Dark Mode Dashboard Theme + * + * A sleek dark theme with cyan accents designed for the py4web dashboard. + * Provides a modern, low-light interface perfect for extended viewing sessions. + * + * CSS Variable Dependencies: + * All styling uses CSS custom properties defined in :root below. + * To customize this theme, modify the variables rather than hardcoding colors. + * + * File Structure: + * 1. CSS Variables (:root) - Color palette and design tokens + * 2. Base Elements - html, body, and general text styling + * 3. Form Elements - input, textarea, select, button styling + * 4. Content - Links, panels, modals, and general content + * 5. Tables & Database - Grid and dbadmin specific overrides + * 6. Navigation - Header, nav, footer styling + */ + +/* ============================================================================ + 1. CSS VARIABLES - Color Palette & Design Tokens + ========================================================================== */ +:root { + --bg-primary: black; /* Main page background */ + --text-primary: #d1d1d1; /* Main text color (light gray) */ + --accent: #33BFFF; /* Primary accent color (cyan) */ + --accent-dark: #139FDF; /* Darker accent for hover states */ + --bg-secondary: #222; /* Secondary background (dark gray) */ + --border-color: #33BFFF; /* Border and divider color (matches accent) */ +} + +html { + background: var(--bg-primary); + color: var(--text-primary); +} + +body { + background: var(--bg-primary); + color: var(--text-primary); +} + +/* ============================================================================ + 3. FORM ELEMENTS - Inputs, Textareas, Selects, and Buttons + ========================================================================== */ + +button { + background-color: var(--bg-primary); + color: var(--accent); + border: 2px solid var(--accent); + box-shadow: inset 0 0 0 0 var(--accent); +} + +button:hover { + border: 2px solid var(--accent-dark); + box-shadow: inset 0 0 0 50px var(--accent); + color: black; +} + +input[type=text], +input[type=password], +input[type=number], +input[type=date], +input[type=time], +input[type=datetime-local], +input[type=file], +select, +textarea { + background-color: var(--bg-primary); + color: var(--accent); +} + +input[type=text], +input[type=password], +input[type=number], +input[type=date], +input[type=time], +input[type=datetime-local], +select, +textarea { + border-bottom: 2px solid var(--accent); +} + +/* ============================================================================ + 4. CONTENT - Links, Panels, Modals, and Interactive Elements + ========================================================================== */ +.my-effects a, .my-effects a:hover, .my-effects a:visited { + color: var(--accent); +} + +.my-effects a:not(.btn):after { + background-color: var(--accent); +} + +.panel > label { + border: 4px solid var(--accent); + background: linear-gradient(to right, var(--accent-dark), var(--accent)); +} + +.modal-inner { + border: 5px solid var(--accent); +} + +.login { + color: var(--accent); +} + +.login input[type=password] { + border: 2px solid var(--accent); +} + +.loading { + background-color: var(--bg-primary); +} + +.my-effects .accordion>label:before, +.my-effects .accordion>input:checked ~ label:before { + color: var(--accent); +} + +tbody { + border-bottom: 1px solid var(--accent); +} + +/* ============================================================================ + 5. TABLES & DATABASE GRID - Grid and DBAdmin styling + ========================================================================== */ +.dbadmin tbody tr:hover { + background-color: #002233; +} + +thead>tr { + background: var(--bg-primary); + border-top: 2px solid var(--accent); + border-bottom: 2px solid var(--accent); +} + +/* ============================================================================ + 6. BUTTONS & NAVIGATION - Button, nav, and header styling + ========================================================================== */ +button, a[role=button], input[type=submit], input[type=button] { + background-color: var(--accent); + color: black; +} + +nav.black a, a { + color: var(--accent); +} + +label ~ div { + background-color: hsl(0, 0%, 10%); +} + +header { + background-color: #111; +} + +nav:not(.black) { + background-color: #111; +} + +select { + background-color: var(--bg-secondary) !important; + color: white !important; +} + +select option { + background-color: var(--bg-secondary); + color: white; +} diff --git a/apps/_dashboard/static/themes/AlienLight/theme.css b/apps/_dashboard/static/themes/AlienLight/theme.css new file mode 100644 index 000000000..c485077c6 --- /dev/null +++ b/apps/_dashboard/static/themes/AlienLight/theme.css @@ -0,0 +1,335 @@ +/** + * AlienLight Theme - Light Mode Dashboard Theme + * + * An elegant light theme with blue accents designed for the py4web dashboard. + * Provides excellent readability with high contrast text on light backgrounds. + * + * CSS Variable Dependencies: + * All styling uses CSS custom properties defined in :root below. + * To customize this theme, modify the variables rather than hardcoding colors. + * + * File Structure: + * 1. CSS Variables (:root) - Color palette and design tokens + * 2. Base Elements - html, body, and general text styling + * 3. Form Elements - input, textarea, select, button styling + * 4. Navigation - header, nav, footer styling + * 5. Content - Links, panels, modals, and general content + * 6. Database/Table - Grid and dbadmin specific overrides + * 7. Interactive - Hover states and focus states + */ + +/* ============================================================================ + 1. CSS VARIABLES - Color Palette & Design Tokens + ========================================================================== */ +:root { + --bg-primary: white; /* Main page background */ + --text-primary: #333; /* Main text color */ + --accent: #0074d9; /* Primary accent color (blue) */ + --accent-dark: #0052a3; /* Darker accent for hover states */ + --bg-secondary: #f5f5f5; /* Secondary background (light gray) */ + --border-color: #d1d1d1; /* Border and divider color */ +} + +/* ============================================================================ + 2. BASE ELEMENTS - Document Root, Body, and General Styling + ========================================================================== */ +html { + background: var(--bg-primary); + color: var(--text-primary); +} + +body { + background: var(--bg-primary); + color: var(--text-primary); +} + +html, body { + min-height: 100%; +} + +.my-effects { + background: var(--bg-primary); + min-height: 100vh; +} + +/* ============================================================================ + 3. FORM ELEMENTS - Inputs, Textareas, Selects, and Buttons + ========================================================================== */ + +button { + background-color: var(--accent); + color: white; + border: 2px solid var(--accent); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +button:hover { + background-color: var(--accent-dark); + border: 2px solid var(--accent-dark); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +input[type=text], +input[type=password], +input[type=number], +input[type=date], +input[type=time], +input[type=datetime-local], +input[type=file], +select, +textarea { + background-color: white; + color: var(--text-primary); + border: 1px solid var(--border-color); +} + +input[type=text]:focus, +input[type=password]:focus, +input[type=number]:focus, +input[type=date]:focus, +input[type=time]:focus, +input[type=datetime-local]:focus, +select:focus, +textarea:focus { + border: 1px solid var(--accent); + box-shadow: 0 0 5px rgba(0, 116, 217, 0.3); +} + +/* ============================================================================ + 4. NAVIGATION - Header, Nav, and Footer + ========================================================================== */ +.my-effects a, .my-effects a:hover, .my-effects a:visited { + color: var(--accent); +} + +.my-effects a:not(.btn):after { + background-color: var(--accent); +} + +.panel > label { + border: 2px solid var(--accent); + background: linear-gradient(to right, #e3f2fd, #bbdefb); + color: var(--text-primary); +} + +.modal-inner { + border: 2px solid var(--accent); + background-color: white; +} + +/* ============================================================================ + 5. CONTENT - Login and Interactive Elements + ========================================================================== */ + +.login { + background: rgba(247, 247, 247, 0.9); + color: var(--accent); +} + +.login h1, .login h2 { + color: var(--accent); +} + +.login input[type=password] { + border: 2px solid var(--accent); + background-color: white; + color: var(--text-primary); +} + +.loading { + background-color: var(--bg-primary); +} + +tbody { + border-bottom: 1px solid var(--border-color); +} + +tbody tr:hover { + background-color: #f9f9f9; +} + +/* ============================================================================ + 6. TABLES & DATABASE GRID - DBAdmin and table styling + ========================================================================== */ +.dbadmin tbody tr:hover { + background-color: #f0f0f0; +} + +thead tr { + background: var(--bg-secondary); + border-top: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); +} + +thead th { + background-color: var(--bg-secondary); + color: var(--text-primary); + border-bottom: 1px solid var(--border-color); +} + +button, a[role=button], input[type=submit], input[type=button] { + background-color: var(--accent); + color: white; +} + +/* ============================================================================ + 7. NAVIGATION & INTERACTIVE - Header, nav, footer, and interactive elements + ========================================================================== */ +nav.black a, a { + color: var(--accent); +} + +label ~ div { + background-color: white; + border: 1px solid var(--border-color); +} + +header { + background-color: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); +} + +nav { + background-color: var(--bg-secondary); +} + +header.black, +footer.black, +nav.black { + background-color: var(--bg-secondary); + color: var(--text-primary); +} + +nav.black a, +footer.black a { + color: var(--accent); +} + +.spinner-top { + background-color: white; +} + +nav li:hover { + background-color: #e8e8e8; +} + +select { + background-color: white !important; + color: var(--text-primary) !important; +} + +select option { + background-color: white; + color: var(--text-primary); +} + +label:not(.help) { + color: var(--text-primary); + background-color: var(--bg-secondary); +} + +.tag { + background-color: var(--accent); + color: white; +} + +tr:hover { + background-color: #f5f5f5; +} + +/* ============================================================================ + 8. DBADMIN-SPECIFIC OVERRIDES - Comprehensive styling for dbadmin pages + ========================================================================== */ + +/* Ensure all containers and content areas are light */ +main, +article, +section, +.container, +.content, +.grid, +.form-container { + background-color: var(--bg-primary) !important; + color: var(--text-primary) !important; +} + +/* Form wrappers and content divs */ +.form-group, +.form-wrapper, +.fieldset { + background-color: white !important; + color: var(--text-primary) !important; +} + +/* Ensure all input types have proper styling */ +input[type=search], +input[type=email], +input[type=url] { + background-color: white !important; + color: var(--text-primary) !important; + border: 1px solid var(--border-color) !important; +} + +/* Ensure checkboxes and radio buttons visibility */ +input[type=checkbox], +input[type=radio] { + background-color: white !important; +} + +/* Table styling - ensure visibility */ +table { + background-color: white !important; + color: var(--text-primary) !important; +} + +/* Grid and data cells */ +td, +.grid-cell { + background-color: white !important; + color: var(--text-primary) !important; +} + +th, +.grid-header { + background-color: var(--bg-secondary) !important; + color: var(--text-primary) !important; +} + +/* Panel and card backgrounds */ +.panel, +.card, +.box { + background-color: var(--bg-secondary) !important; + color: var(--text-primary) !important; + border: 1px solid var(--border-color) !important; +} + +/* Ensure light background for any dark elements */ +.dark, +.bg-dark, +[style*="background: black"], +[style*="background-color: black"] { + background-color: var(--bg-primary) !important; + color: var(--text-primary) !important; +} + +/* Remove any dark overlays */ +.overlay, +.modal-backdrop { + background-color: rgba(0, 0, 0, 0.1) !important; +} + +/* Ensure list items are properly colored */ +li { + color: var(--text-primary); +} + +/* Ensure all text content is readable */ +p, span, div { + color: var(--text-primary); +} + +/* Style favicon icons to green */ +img[src*="favicon.ico"] { + filter: hue-rotate(110deg) saturate(1.5) brightness(0.8); +} diff --git a/apps/_dashboard/templates/dbadmin.html b/apps/_dashboard/templates/dbadmin.html index a032bf6fa..aef7d9769 100644 --- a/apps/_dashboard/templates/dbadmin.html +++ b/apps/_dashboard/templates/dbadmin.html @@ -1,6 +1,6 @@ [[extend "layout.html"]]
-

Table "[[=table_name]]"

+

Application "[[=app_name]]" - Table "[[=table_name]]"

[[=grid]]
diff --git a/apps/_dashboard/templates/index.html b/apps/_dashboard/templates/index.html index 9647825f8..a0bd7affb 100644 --- a/apps/_dashboard/templates/index.html +++ b/apps/_dashboard/templates/index.html @@ -3,9 +3,11 @@ - + + + @@ -31,10 +33,18 @@

-
- - -
+
+
+ + +
+
+ + +
+

API error


diff --git a/apps/_dashboard/templates/layout.html b/apps/_dashboard/templates/layout.html
index bea5e69b4..62e6a9f2c 100644
--- a/apps/_dashboard/templates/layout.html
+++ b/apps/_dashboard/templates/layout.html
@@ -3,18 +3,29 @@
   
     
     
-    
+    
     
+    
     
+    
     [[block page_head]][[end]]
   
   
     
-
@@ -29,10 +40,11 @@ -
+ diff --git a/apps/_dashboard/templates/ticket.html b/apps/_dashboard/templates/ticket.html index 5bc5afafe..c8336544d 100644 --- a/apps/_dashboard/templates/ticket.html +++ b/apps/_dashboard/templates/ticket.html @@ -1,5 +1,10 @@ + + + + +