diff --git a/.gitignore b/.gitignore
index 4f8ca2d8..f58148c8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,3 +53,5 @@ monkeytype.sqlite3
# Test related data
temp/
+
+config.py
\ No newline at end of file
diff --git a/config_sample.py b/config_sample_backup.py
similarity index 100%
rename from config_sample.py
rename to config_sample_backup.py
diff --git a/database.py b/database.py
index 8248e2d7..70e4754f 100755
--- a/database.py
+++ b/database.py
@@ -60,13 +60,16 @@ def create_session(db_string: str, drop_tables: bool = False) -> scoped_session:
)
else:
db_engine = create_engine(db_string)
- db_session = scoped_session(sessionmaker(bind=db_engine))
- Base.query = db_session.query_property()
- if drop_tables:
- Base.metadata.drop_all(bind=db_engine)
+ if drop_tables:
+ Base.metadata.drop_all(bind=db_engine)
+
+ # Only create tables once when the engine is first initialised,
+ # not on every request — avoids connection pool exhaustion.
+ Base.metadata.create_all(bind=db_engine)
- Base.metadata.create_all(bind=db_engine)
+ db_session = scoped_session(sessionmaker(bind=db_engine))
+ Base.query = db_session.query_property()
return db_session
except SQLAlchemyError:
diff --git a/install/init_db.py b/install/init_db.py
index b8bdf1de..0549299d 100644
--- a/install/init_db.py
+++ b/install/init_db.py
@@ -29,3 +29,8 @@ def run():
run()
+
+
+
+
+# python install/init_db.py mysql2://root:password@localhost:3306/ccextractor?charset=utf8 test test@test.com testpassword
\ No newline at end of file
diff --git a/install/install.sh b/install/install.sh
index 799163c6..00877f51 100644
--- a/install/install.sh
+++ b/install/install.sh
@@ -11,11 +11,11 @@ clear
date=$(date +%Y-%m-%d-%H-%M)
install_log="${dir}/PlatformInstall_${date}_log.txt"
echo "Welcome to the CCExtractor platform installer!"
-if [[ "$EUID" -ne 0 ]]
- then
- echo "You must be a root user to install CCExtractor platform." 2>&1
- exit -1
-fi
+ # macOS does not require root for most installs (Homebrew handles permissions)
+ if ! command -v brew >/dev/null 2>&1; then
+ echo "Homebrew is required but not installed. Install it from https://brew.sh/"
+ exit -1
+ fi
echo ""
echo "Detailed information will be written to $install_log"
echo "Please read the installation instructions carefully before installing."
@@ -24,23 +24,17 @@ echo "-------------------------------"
echo "| Installing dependencies |"
echo "-------------------------------"
echo ""
-echo "* Updating package list"
-apt-get update >> "$install_log" 2>&1
-echo "* Installing nginx, python, pip, mediainfo and gunicorn"
-add-apt-repository ppa:deadsnakes/ppa -y >> "$install_log" 2>&1
-apt-get -q -y install python3.9 nginx python3.9-distutils python3-pip mediainfo gunicorn3 >> "$install_log" 2>&1
-update-alternatives --install /usr/bin/python python /usr/bin/python3.9 1 >> "$install_log" 2>&1
-update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.9 1 >> "$install_log" 2>&1
-rm -f /etc/nginx/sites-available/default
-rm -f /etc/nginx/sites-enabled/default
-if [ ! -f /etc/init.d/mysql* ]; then
- echo "* Installing MySQL (root password will be empty!)"
- apt-get install -y mysql-server >> "$install_log" 2>&1
- if [ ! -f /etc/init.d/mysql* ]; then
- echo "Failed to install MySQL! Please check the installation log!"
- exit -1
- fi
-fi
+echo "* Updating Homebrew"
+brew update >> "$install_log" 2>&1
+
+echo "* Installing nginx, python, mediainfo and mysql using Homebrew"
+brew install python@3.11 nginx mediainfo mysql >> "$install_log" 2>&1
+
+echo "* Installing gunicorn"
+python3 -m pip install gunicorn >> "$install_log" 2>&1
+
+echo "* Starting MySQL using brew services"
+brew services start mysql >> "$install_log" 2>&1
RED='\033[0;31m'
NC='\033[0m'
@@ -259,16 +253,11 @@ LINUX_INSTANCE_FAMILY_NAME = '${linux_instance_family_name}'
GCP_INSTANCE_MAX_RUNTIME = $gcp_instance_max_runtime # In minutes
GCS_BUCKET_NAME = '${gcs_bucket_name}'
GCS_SIGNED_URL_EXPIRY_LIMIT = $signed_url_expiry_time # In minutes" > "${dir}/../config.py"
-# Ensure the files are executable by www-data
-chown -R www-data:www-data "${root_dir}" "${sample_repository}"
+ # macOS does not use www-data user by default
+echo "Skipping www-data ownership change on macOS"
echo "* Creating startup script"
-{
- cp "${dir}/platform" /etc/init.d/platform
- sed -i "s#BASE_DIR#${root_dir}#g" /etc/init.d/platform
- chmod 755 /etc/init.d/platform
- update-rc.d platform defaults
-} >> "$install_log" 2>&1
+echo "Skipping init.d service creation (not supported on macOS)"
echo "* Creating RClone config file"
{
@@ -281,12 +270,12 @@ echo "* Creating RClone config file"
echo "* Creating Nginx config"
{
- cp "${dir}/nginx.conf" /etc/nginx/sites-available/platform
- sed -i "s/NGINX_HOST/${config_server_name}/g" /etc/nginx/sites-available/platform
- sed -i "s#NGINX_CERT#${config_ssl_cert}#g" /etc/nginx/sites-available/platform
- sed -i "s#NGINX_KEY#${config_ssl_key}#g" /etc/nginx/sites-available/platform
- sed -i "s#NGINX_DIR#${root_dir}#g" /etc/nginx/sites-available/platform
- ln -s /etc/nginx/sites-available/platform /etc/nginx/sites-enabled/platform
+ mkdir -p /opt/homebrew/etc/nginx/servers
+ cp "${dir}/nginx.conf" /opt/homebrew/etc/nginx/servers/platform.conf
+ sed -i '' "s/NGINX_HOST/${config_server_name}/g" /opt/homebrew/etc/nginx/servers/platform.conf
+ sed -i '' "s#NGINX_CERT#${config_ssl_cert}#g" /opt/homebrew/etc/nginx/servers/platform.conf
+ sed -i '' "s#NGINX_KEY#${config_ssl_key}#g" /opt/homebrew/etc/nginx/servers/platform.conf
+ sed -i '' "s#NGINX_DIR#${root_dir}#g" /opt/homebrew/etc/nginx/servers/platform.conf
} >> "$install_log" 2>&1
echo "* Moving variables and runCI files"
@@ -295,8 +284,9 @@ echo "* Moving variables and runCI files"
cp $root_dir/install/ci-vm/ci-linux/ci/* "${sample_repository}/TestData/ci-linux/"
} >> "$install_log" 2>&1
echo "* Reloading nginx"
-service nginx reload >> "$install_log" 2>&1
+brew services restart nginx >> "$install_log" 2>&1
echo ""
echo "* Starting Platform..."
-service platform start
+echo "Platform service auto-start not supported on macOS. Run the platform manually."
echo "Platform installed!"
+
diff --git a/requirements.txt b/requirements.txt
index a6440a12..87cabe2d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,29 +1,243 @@
-sqlalchemy==2.0.48
-flask==3.1.3
-passlib==1.7.4
-pymysql==1.1.2
-python-magic==0.4.27
-flask-wtf==1.2.2
-requests==2.32.5
-pyIsEmail==2.0.1
-GitPython==3.1.46
-xmltodict==1.0.4
-lxml==6.0.2
-pytz==2026.1.post1
-tzlocal==5.3.1
-markdown2==2.5.5
-flask-migrate==4.1.0
-email_validator
+alembic==1.18.4
+annotated-doc==0.0.4
+annotated-types==0.7.0
+anyio==4.12.1
+asgiref==3.11.1
+astroid==2.15.8
+asttokens==3.0.1
+attrs==25.4.0
+autopep8==2.3.2
+bcrypt==5.0.0
+beautifulsoup4==4.12.2
+black==25.9.0
+blinker==1.9.0
+build==1.4.0
+certifi==2025.8.3
+cffi==2.0.0
+cfgv==3.5.0
+charset-normalizer==2.0.12
+cheroot==10.0.1
+chromadb==1.5.4
+click==8.1.8
+colorama==0.4.6
+coloredlogs==15.0.1
+colorlog==6.8.2
+configobj==5.0.8
+coreapi==2.3.3
+coreschema==0.0.4
+coverage==7.10.6
+cryptography==46.0.5
+decorator==5.2.1
+dill==0.3.6
+diskcache==5.6.3
+distlib==0.4.0
+Django==3.2.25
+django-filter==21.1
+django-ipware==4.0.2
+django-js-asset==2.2.0
+django-js-reverse==0.10.2
+django-mptt==0.14.0
+django-redis-cache==3.0.1
+django-silk==5.2.0
+django-sortedm2m==3.1.1
+django_csp==3.8
+djangorestframework==3.14.0
+dnspython==2.8.0
+drf-yasg==1.21.7
+durationpy==0.10
+esprima==4.0.1
+exceptiongroup==1.2.2
+execnet==2.1.2
+executing==2.2.1
+filelock==3.25.0
+filetype==1.2.0
+Flask==3.1.0
+flask-cors==5.0.1
+Flask-Migrate==4.1.0
+Flask-SQLAlchemy==3.1.1
+Flask-WTF==1.2.2
+flatbuffers==25.12.19
+fsspec==2026.2.0
gitdb==4.0.12
-Werkzeug==3.1.6
-WTForms==3.2.1
-MarkupSafe==3.0.3
-jinja2==3.1.6
-itsdangerous==2.2.0
+GitPython==3.1.46
+google-api-core==2.30.0
google-api-python-client==2.192.0
+google-auth==2.49.1
+google-auth-httplib2==0.3.0
+google-cloud-core==2.5.0
google-cloud-storage==3.9.0
-cffi==2.0.0
+google-crc32c==1.8.0
+google-resumable-media==2.8.0
+googleapis-common-protos==1.73.0
+gprof2dot==2025.4.14
+grpcio==1.78.0
+h11==0.16.0
+hf-xet==1.3.2
+html5lib==1.1
+httpcore==1.0.9
+httplib2==0.31.2
+httptools==0.7.1
+httpx==0.28.1
+huggingface_hub==1.6.0
+humanfriendly==10.0
+hypothesis==6.151.9
+identify==2.6.17
+idna==3.7
+ifaddr==0.1.7
+ifcfg==0.24
+importlib-metadata==4.8.3
+importlib-resources==5.4.0
+inflect==7.5.0
+inflection==0.5.1
+iniconfig==2.1.0
+ipdb==0.13.13
+ipython==8.38.0
+isort==5.13.2
+itsdangerous==2.2.0
+itypes==1.2.0
+jaraco.functools==4.4.0
+jedi==0.19.2
+Jinja2==3.1.6
+joblib==1.5.3
+json-schema-validator==2.4.1
+jsonfield==3.1.0
+jsonschema==4.26.0
+jsonschema-specifications==2025.9.1
+# Editable install with no version control (kolibri==0.20.0.dev0+git.20260219010113)
+-e /Users/hemant/Desktop/opensource/kolibri
+kubernetes==35.0.0
+lazy-object-proxy==1.12.0
+le-utils==0.2.13
+lxml==6.0.2
+MagicBus==4.1.2
+Mako==1.3.10
+markdown-it-py==4.0.0
+MarkupSafe==3.0.2
+matplotlib-inline==0.2.1
+mccabe==0.7.0
+mdurl==0.1.2
+mmh3==5.2.1
+morango==0.8.7
+more-itertools==10.8.0
+mpmath==1.3.0
+mypy==1.0.1
+mypy_extensions==0.4.4
+networkx==3.4.2
+nodeenv==1.10.0
+numpy==2.2.6
+oauthlib==3.3.1
+onnxruntime==1.23.2
+opentelemetry-api==1.40.0
+opentelemetry-exporter-otlp-proto-common==1.40.0
+opentelemetry-exporter-otlp-proto-grpc==1.40.0
+opentelemetry-proto==1.40.0
+opentelemetry-sdk==1.40.0
+opentelemetry-semantic-conventions==0.61b0
+orjson==3.11.7
+overrides==7.7.0
+packaging==25.0
+parso==0.8.6
+passlib==1.7.4
+pathspec==0.12.1
+pexpect==4.9.0
+Pillow==10.1.0
+pip-tools==7.5.2
+platformdirs==4.9.4
+pluggy==1.6.0
+pre_commit==4.5.1
+prompt_toolkit==3.0.52
+proto-plus==1.27.1
+protobuf==6.33.5
+psutil==7.0.0
+ptyprocess==0.7.0
+pure_eval==0.2.3
+py==1.11.0
+pyasn1==0.6.2
+pyasn1_modules==0.4.2
+pybase64==1.4.3
+pycodestyle==2.14.0
+pycparser==3.0
+pydantic==2.12.5
+pydantic-settings==2.13.1
+pydantic_core==2.41.5
PyGithub==2.8.1
-blinker==1.9.0
-click==8.3.1
-PyYAML==6.0.3
+Pygments==2.19.2
+pyIsEmail==2.0.1
+PyJWT==2.12.0
+pylint==2.17.7
+pylint-quotes==0.2.3
+PyNaCl==1.6.2
+pyparsing==3.3.2
+PyPika==0.51.1
+pyproject_hooks==1.2.0
+pytest==8.4.2
+pytest-cov==4.1.0
+pytest-timeout==2.2.0
+pytest-xdist==3.5.0
+python-dateutil==2.9.0.post0
+python-discovery==1.1.3
+python-dotenv==1.2.2
+pytokens==0.1.10
+pytz==2024.1
+pytz-deprecation-shim==0.1.0.post0
+PyYAML==6.0.2
+rcssmin==1.2.1
+redis==3.5.3
+referencing==0.37.0
+regex==2026.2.28
+requests==2.27.1
+requests-oauthlib==2.0.0
+rich==14.3.3
+rpds-py==0.30.0
+rsa==4.9.1
+safetensors==0.7.0
+scikit-learn==1.7.2
+scipy==1.15.3
+semver==2.13.0
+sentence-transformers==5.2.3
+shellingham==1.5.4
+six==1.17.0
+smmap==5.0.3
+sortedcontainers==2.4.0
+soupsieve==2.4.1
+sqlacodegen==2.3.0.post1
+SQLAlchemy==2.0.48
+sqlparse==0.5.5
+stack-data==0.6.3
+sympy @ file:///Users/hemant/Desktop/opensource/sympy
+tenacity==9.1.4
+threadpoolctl==3.6.0
+tokenizers==0.22.2
+tomli==2.4.0
+tomlkit==0.11.8
+torch==2.10.0
+tox==3.28.0
+tqdm==4.67.3
+traitlets==5.14.3
+transformers==5.3.0
+typeguard==4.5.1
+typer==0.24.1
+typing-inspection==0.4.2
+typing_extensions==4.15.0
+tzdata==2025.3
+tzlocal==4.2
+uritemplate==4.2.0
+urllib3==1.26.20
+uvicorn==0.41.0
+uvloop==0.22.1
+virtualenv==21.2.0
+waitress==3.0.2
+watchfiles==1.1.1
+wcwidth==0.6.0
+webencodings==0.5.1
+WebOb==1.8.7
+websocket-client==1.9.0
+websockets==16.0
+WebTest==3.0.6
+Werkzeug==3.1.3
+whitenoise==5.3.0
+wrapt==1.17.3
+WTForms==3.2.1
+xmltodict==0.15.0
+zeroconf-py2compat==0.19.17
+zipp==3.23.0
diff --git a/run.py b/run.py
index e277c6d9..2c183207 100755
--- a/run.py
+++ b/run.py
@@ -34,16 +34,17 @@
from mod_test.controllers import mod_test
from mod_upload.controllers import mod_upload
+
+import config as config_module
+print("DEBUG: config.py loaded from:", config_module.__file__)
+print("DEBUG: INSTALL_FOLDER value:", config_module.INSTALL_FOLDER)
app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app) # type: ignore[method-assign]
-# Load config
-try:
- config = parse_config('config')
-except ImportStringError:
- traceback.print_exc()
- raise MissingConfigError()
-app.config.from_mapping(config)
+# Load config directly from config.py
+import config as config_module
+config_dict = {k: getattr(config_module, k) for k in dir(config_module) if k.isupper()}
+app.config.from_mapping(config_dict)
app.config['DEBUG'] = os.environ.get('DEBUG', False)
# embed flask-migrate in the app itself
@@ -60,12 +61,13 @@
app.config['DEBUG'])
log = log_configuration.create_logger("Platform")
-# Create bucket objext using GCS storage client
-sa_file = os.path.join(app.config.get('INSTALL_FOLDER', ''), app.config.get('SERVICE_ACCOUNT_FILE', ''))
+# Create bucket object using GCS storage client
+sa_file = app.config.get('SERVICE_ACCOUNT_FILE', '')
storage_client = Client.from_service_account_json(sa_file)
storage_client_bucket = storage_client.bucket(app.config.get('GCS_BUCKET_NAME', ''))
# Save build commit
+print('DEBUG: INSTALL_FOLDER value:', app.config.get('INSTALL_FOLDER', ''))
repo = git.Repo(app.config.get('INSTALL_FOLDER', ''))
app.config['BUILD_COMMIT'] = repo.head.object.hexsha
@@ -261,7 +263,7 @@ def teardown(exception: Optional[Exception]):
db = g.get('db', None)
if db is not None:
db.remove()
-
+print("INSTALL_FOLDER:", app.config.get('INSTALL_FOLDER', ''))
# Register blueprints
app.register_blueprint(mod_auth, url_prefix='/account')
diff --git a/static/css/modern.css b/static/css/modern.css
new file mode 100644
index 00000000..b2b416cd
--- /dev/null
+++ b/static/css/modern.css
@@ -0,0 +1,932 @@
+/* ================================================================
+ CCExtractor CI Platform — Modern UI
+ Replaces Zurb Foundation with CSS variables + custom design
+ ================================================================ */
+
+/* ── 1. CSS Custom Properties ─────────────────────────────────── */
+:root {
+ /* Brand */
+ --color-primary: #6366f1;
+ --color-primary-hover: #4f46e5;
+ --color-primary-muted: rgba(99, 102, 241, 0.12);
+ --color-success: #22c55e;
+ --color-warning: #f59e0b;
+ --color-danger: #ef4444;
+ --color-info: #3b82f6;
+
+ /* Surfaces */
+ --bg-base: #f8fafc;
+ --bg-surface: #ffffff;
+ --bg-muted: #f1f5f9;
+ --bg-nav: #0f172a;
+
+ /* Text */
+ --text-primary: #1e293b;
+ --text-secondary: #475569;
+ --text-muted: #94a3b8;
+ --text-inverse: #f1f5f9;
+
+ /* Borders */
+ --border: #e2e8f0;
+ --border-focus: var(--color-primary);
+
+ /* Shadows */
+ --shadow-sm: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04);
+ --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.08), 0 2px 4px -2px rgba(0,0,0,0.05);
+ --shadow-lg: 0 10px 25px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.05);
+
+ /* Layout */
+ --sidebar-width: 240px;
+ --content-max: 1200px;
+ --radius-sm: 6px;
+ --radius-md: 10px;
+ --radius-lg: 16px;
+ --transition: 0.18s ease;
+}
+
+/* Dark mode via CSS custom properties */
+@media (prefers-color-scheme: dark) {
+ :root {
+ --bg-base: #0f172a;
+ --bg-surface: #1e293b;
+ --bg-muted: #273549;
+ --bg-nav: #020617;
+ --text-primary: #f1f5f9;
+ --text-secondary: #94a3b8;
+ --text-muted: #64748b;
+ --border: #334155;
+ --shadow-sm: 0 1px 3px rgba(0,0,0,0.3);
+ --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.4);
+ --shadow-lg: 0 10px 25px -3px rgba(0,0,0,0.5);
+ }
+}
+
+/* Manual dark toggle */
+[data-theme="dark"] {
+ --bg-base: #0f172a;
+ --bg-surface: #1e293b;
+ --bg-muted: #273549;
+ --bg-nav: #020617;
+ --text-primary: #f1f5f9;
+ --text-secondary: #94a3b8;
+ --text-muted: #64748b;
+ --border: #334155;
+ --shadow-sm: 0 1px 3px rgba(0,0,0,0.3);
+ --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.4);
+}
+[data-theme="light"] {
+ --bg-base: #f8fafc;
+ --bg-surface: #ffffff;
+ --bg-muted: #f1f5f9;
+ --bg-nav: #0f172a;
+ --text-primary: #1e293b;
+ --text-secondary: #475569;
+ --text-muted: #94a3b8;
+ --border: #e2e8f0;
+}
+
+/* ── 2. Reset & Base ──────────────────────────────────────────── */
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+html { font-size: 16px; scroll-behavior: smooth; -webkit-font-smoothing: antialiased; }
+
+body {
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background: var(--bg-base);
+ color: var(--text-primary);
+ line-height: 1.6;
+ min-height: 100vh;
+ transition: background-color var(--transition), color var(--transition);
+}
+
+a {
+ color: var(--color-primary);
+ text-decoration: none;
+ transition: color var(--transition), opacity var(--transition);
+}
+a:hover { color: var(--color-primary-hover); }
+
+img { max-width: 100%; height: auto; }
+
+/* ── 3. Typography ────────────────────────────────────────────── */
+h1, h2, h3, h4, h5, h6 {
+ font-weight: 600;
+ line-height: 1.3;
+ color: var(--text-primary);
+ margin-bottom: 0.5em;
+}
+h1 { font-size: 1.875rem; letter-spacing: -0.02em; }
+h2 { font-size: 1.5rem; letter-spacing: -0.01em; }
+h3 { font-size: 1.25rem; }
+h4 { font-size: 1.1rem; }
+h5 { font-size: 0.95rem; font-weight: 600; }
+h6 { font-size: 0.875rem; }
+
+p { margin-bottom: 1rem; color: var(--text-secondary); }
+ul, ol { padding-left: 1.5rem; margin-bottom: 1rem; }
+li { margin-bottom: 0.2rem; color: var(--text-secondary); }
+
+code, pre {
+ font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
+}
+code {
+ font-size: 0.875em;
+ background: var(--bg-muted);
+ color: var(--color-primary);
+ padding: 0.15em 0.45em;
+ border-radius: var(--radius-sm);
+ border: 1px solid var(--border);
+}
+pre {
+ background: var(--bg-muted);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ padding: 1rem 1.25rem;
+ overflow-x: auto;
+ font-size: 0.875rem;
+ color: var(--text-primary);
+ margin-bottom: 1rem;
+}
+pre code { background: none; border: none; padding: 0; color: inherit; }
+
+/* ── 4. Layout ────────────────────────────────────────────────── */
+/* Sidebar layout: content offset by sidebar width on large screens */
+.page-content {
+ margin-left: var(--sidebar-width);
+ padding: 2rem 2rem 5rem;
+ transition: margin-left var(--transition);
+}
+@media (max-width: 900px) {
+ .page-content {
+ margin-left: 0;
+ padding: 4.5rem 1.25rem 5rem;
+ }
+}
+
+/* Foundation grid compatibility layer */
+.grid-x {
+ display: flex;
+ flex-wrap: wrap;
+ margin-left: -0.625rem;
+ margin-right: -0.625rem;
+}
+.cell, .columns, .column {
+ flex: 1 1 100%;
+ padding: 0 0.625rem;
+ word-wrap: break-word;
+ min-width: 0;
+}
+@media (min-width: 640px) {
+ .small-4 { flex: 0 0 33.333%; max-width: 33.333%; }
+ .small-6 { flex: 0 0 50%; max-width: 50%; }
+ .small-8 { flex: 0 0 66.666%; max-width: 66.666%; }
+ .small-12 { flex: 0 0 100%; max-width: 100%; }
+}
+@media (min-width: 768px) {
+ .medium-2 { flex: 0 0 16.666%; max-width: 16.666%; }
+ .medium-3 { flex: 0 0 25%; max-width: 25%; }
+ .medium-4 { flex: 0 0 33.333%; max-width: 33.333%; }
+ .medium-5 { flex: 0 0 41.666%; max-width: 41.666%; }
+ .medium-6 { flex: 0 0 50%; max-width: 50%; }
+ .medium-7 { flex: 0 0 58.333%; max-width: 58.333%; }
+ .medium-8 { flex: 0 0 66.666%; max-width: 66.666%; }
+ .medium-9 { flex: 0 0 75%; max-width: 75%; }
+ .medium-10 { flex: 0 0 83.333%; max-width: 83.333%; }
+ .medium-12 { flex: 0 0 100%; max-width: 100%; }
+}
+
+.row {
+ max-width: var(--content-max);
+ margin: 0 auto;
+}
+
+/* ── 5. Vertical Sidebar Navigation ──────────────────────────── */
+.sidebar {
+ position: fixed;
+ top: 0; left: 0; bottom: 0;
+ width: var(--sidebar-width);
+ background: var(--bg-nav);
+ display: flex;
+ flex-direction: column;
+ z-index: 1000;
+ border-right: 1px solid rgba(255,255,255,0.06);
+ overflow-y: auto;
+ overflow-x: hidden;
+ transition: transform var(--transition);
+}
+
+/* Brand area at top of sidebar */
+.nav-brand {
+ display: flex;
+ align-items: center;
+ gap: 0.1rem;
+ padding: 1.6rem 3.4rem 1rem;
+ font-size: 0.9375rem;
+ font-weight: 700;
+ color: #fff !important;
+ text-decoration: none !important;
+ letter-spacing: -0.02em;
+ border-bottom: 1px solid rgba(255,255,255,0.06);
+ flex-shrink: 0;
+ scale: 1.5;
+}
+.nav-brand .brand-accent { color: var(--color-primary); padding-top: 1.05px;}
+.nav-brand i { font-size: 1rem; color: var(--color-primary); }
+
+/* Vertical links list */
+.nav-links {
+ list-style: none;
+ padding: 1.75rem 1.625rem;
+ margin: 0;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 0.625rem;
+}
+
+.nav-links a {
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+ padding: 0.5rem 0.75rem;
+ border-radius: var(--radius-sm);
+ color: #94a3b8 !important;
+ font-size: 0.8125rem;
+ font-weight: 500;
+ text-decoration: none !important;
+ transition: color var(--transition), background var(--transition);
+ line-height: 1;
+ white-space: nowrap;
+ scale: 1.2;
+}
+.nav-links a i { width: 1rem; text-align: center; font-size: 0.875rem; flex-shrink: 0; }
+.nav-links a:hover { color: #e2e8f0 !important; background: rgba(255,255,255,0.07); }
+.nav-links li.active > a {
+ color: #fff !important;
+ background: rgba(99,102,241,0.18);
+}
+.nav-links li.active > a i { color: var(--color-primary); }
+
+/* Submenu (indented, shown on hover or click) */
+.nav-links .has-submenu > a::after {
+ content: '\f107';
+ font-family: 'Font Awesome 6 Free';
+ font-weight: 900;
+ font-size: 0.7rem;
+ margin-left: auto;
+ transition: transform var(--transition);
+}
+.nav-links .has-submenu.open > a::after,
+.nav-links .has-submenu:hover > a::after { transform: rotate(-180deg); }
+
+.nav-links .submenu {
+ display: none;
+ list-style: none;
+ padding: 0.25rem 0 0.25rem 1.75rem;
+ gap: 0.125rem;
+}
+.nav-links .has-submenu:hover .submenu,
+.nav-links .has-submenu.open .submenu { display: flex; flex-direction: column; }
+.nav-links .submenu a {
+ font-size: 0.775rem;
+ padding: 0.4rem 0.6rem;
+ color: #64748b !important;
+}
+.nav-links .submenu a:hover { color: #e2e8f0 !important; }
+
+/* Floating theme toggle — fixed top-right of content area */
+.theme-btn {
+ position: fixed;
+ top: 0.85rem;
+ right: 1.25rem;
+ background: var(--bg-surface);
+ border: 1px solid var(--border);
+ color: var(--text-secondary);
+ width: 36px; height: 36px;
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.9rem;
+ transition: all var(--transition);
+ z-index: 1100;
+ box-shadow: var(--shadow-sm);
+}
+.theme-btn:hover {
+ border-color: var(--color-primary);
+ color: var(--color-primary);
+ box-shadow: var(--shadow-md);
+}
+
+/* Hamburger (mobile only) */
+.nav-hamburger {
+ display: none;
+ position: fixed;
+ top: 0.75rem; left: 0.75rem;
+ z-index: 1100;
+ background: var(--bg-nav);
+ border: 1px solid rgba(255,255,255,0.12);
+ color: #94a3b8;
+ width: 38px; height: 38px;
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ align-items: center;
+ justify-content: center;
+ font-size: 1rem;
+ transition: all var(--transition);
+}
+.nav-hamburger:hover { color: #fff; border-color: var(--color-primary); }
+
+/* Mobile: sidebar slides in as overlay */
+@media (max-width: 900px) {
+ .sidebar {
+ transform: translateX(-100%);
+ box-shadow: none;
+ }
+ .sidebar.open {
+ transform: translateX(0);
+ box-shadow: 4px 0 24px rgba(0,0,0,0.4);
+ }
+ .nav-hamburger { display: flex; }
+ /* dim overlay behind sidebar on mobile */
+ .sidebar-overlay {
+ display: none;
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.5);
+ z-index: 999;
+ }
+ .sidebar-overlay.open { display: block; }
+}
+
+/* ── 6. Buttons ───────────────────────────────────────────────── */
+.button,
+button[type="submit"],
+input[type="submit"] {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ padding: 0.5rem 1.125rem;
+ border-radius: var(--radius-sm);
+ border: 1.5px solid transparent;
+ font-size: 0.875rem;
+ font-weight: 500;
+ font-family: inherit;
+ cursor: pointer;
+ text-decoration: none !important;
+ transition: all var(--transition);
+ background: var(--color-primary);
+ color: #fff !important;
+ line-height: 1.5;
+ white-space: nowrap;
+ margin-bottom: 0;
+}
+.button:hover,
+button[type="submit"]:hover,
+input[type="submit"]:hover {
+ background: var(--color-primary-hover);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 14px rgba(99,102,241,0.35);
+ color: #fff !important;
+}
+.button:active,
+button[type="submit"]:active,
+input[type="submit"]:active { transform: translateY(0); box-shadow: none; }
+
+.button.secondary {
+ background: var(--bg-surface);
+ color: var(--text-primary) !important;
+ border-color: var(--border);
+}
+.button.secondary:hover {
+ border-color: var(--color-primary);
+ color: var(--color-primary) !important;
+ background: var(--color-primary-muted);
+ box-shadow: none;
+ transform: none;
+}
+.button.alert { background: var(--color-danger); }
+.button.alert:hover { background: #dc2626; box-shadow: 0 4px 14px rgba(239,68,68,0.35); }
+.button.success { background: var(--color-success); }
+.button.success:hover { background: #16a34a; }
+.button.warning { background: var(--color-warning); color: #1e293b !important; }
+.button.small { padding: 0.3rem 0.7rem; font-size: 0.8rem; }
+.button.large { padding: 0.65rem 1.5rem; font-size: 1rem; }
+.button.hollow {
+ background: transparent;
+ color: var(--color-primary) !important;
+ border-color: var(--color-primary);
+}
+.button.hollow:hover { background: var(--color-primary-muted); transform: none; box-shadow: none; }
+
+/* ── 7. Forms ─────────────────────────────────────────────────── */
+label {
+ display: block;
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: var(--text-primary);
+ margin-bottom: 0.35rem;
+}
+
+input[type="text"],
+input[type="email"],
+input[type="password"],
+input[type="number"],
+input[type="url"],
+input[type="search"],
+select,
+textarea {
+ display: block;
+ width: 100%;
+ padding: 0.5rem 0.75rem;
+ font-size: 0.875rem;
+ font-family: inherit;
+ color: var(--text-primary);
+ background: var(--bg-surface);
+ border: 1.5px solid var(--border);
+ border-radius: var(--radius-sm);
+ transition: border-color var(--transition), box-shadow var(--transition), background var(--transition);
+ outline: none;
+ margin-bottom: 1rem;
+ line-height: 1.5;
+}
+input[type="text"]:focus,
+input[type="email"]:focus,
+input[type="password"]:focus,
+input[type="number"]:focus,
+input[type="url"]:focus,
+input[type="search"]:focus,
+select:focus,
+textarea:focus {
+ border-color: var(--color-primary);
+ box-shadow: 0 0 0 3px rgba(99,102,241,0.15);
+}
+input.is-invalid-input,
+textarea.is-invalid-input,
+select.is-invalid-input { border-color: var(--color-danger); }
+input.is-invalid-input:focus { box-shadow: 0 0 0 3px rgba(239,68,68,0.15); }
+.is-invalid-label { color: var(--color-danger); }
+
+.form-error { display: none; color: var(--color-danger); font-size: 0.8rem; margin-top: -0.75rem; margin-bottom: 0.75rem; }
+.form-error.is-visible { display: block; }
+.help-text { font-size: 0.8rem; color: var(--text-muted); margin-top: -0.75rem; margin-bottom: 0.75rem; }
+
+textarea { min-height: 100px; resize: vertical; }
+select { cursor: pointer; }
+
+input[type="checkbox"],
+input[type="radio"] {
+ width: auto;
+ margin-bottom: 0;
+ accent-color: var(--color-primary);
+}
+
+/* ── 8. Cards & Callouts ──────────────────────────────────────── */
+.card {
+ background: var(--bg-surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ padding: 1.5rem;
+ box-shadow: var(--shadow-sm);
+ transition: box-shadow var(--transition), transform var(--transition);
+}
+.card:hover { box-shadow: var(--shadow-md); }
+.card-title { font-size: 1rem; font-weight: 600; margin-bottom: 0.75rem; }
+
+.callout {
+ padding: 1rem 1.25rem;
+ border-radius: var(--radius-md);
+ border-left: 4px solid var(--color-info);
+ background: rgba(59,130,246,0.06);
+ margin-bottom: 1rem;
+}
+.callout h4, .callout h5 { color: var(--text-primary); margin-bottom: 0.25rem; }
+.callout p, .callout li { color: var(--text-secondary); margin-bottom: 0; }
+.callout.primary { border-color: var(--color-primary); background: var(--color-primary-muted); }
+.callout.warning { border-color: var(--color-warning); background: rgba(245,158,11,0.07); }
+.callout.alert { border-color: var(--color-danger); background: rgba(239,68,68,0.07); }
+.callout.success { border-color: var(--color-success); background: rgba(34,197,94,0.07); }
+
+/* ── 9. Tables ────────────────────────────────────────────────── */
+.table-wrap {
+ background: var(--bg-surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ overflow: hidden;
+ box-shadow: var(--shadow-sm);
+ margin-bottom: 1.5rem;
+}
+table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.875rem;
+}
+table.stack { width: 100%; }
+thead {
+ background: var(--bg-muted);
+}
+thead th {
+ padding: 0.625rem 0.875rem;
+ text-align: left;
+ font-weight: 600;
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--text-muted);
+ white-space: nowrap;
+ border-bottom: 1px solid var(--border);
+}
+tbody tr {
+ border-bottom: 1px solid var(--border);
+ transition: background var(--transition);
+}
+tbody tr:last-child { border-bottom: none; }
+tbody tr:hover { background: var(--bg-muted); }
+tbody td {
+ padding: 0.625rem 0.875rem;
+ color: var(--text-primary);
+ vertical-align: middle;
+}
+tfoot td {
+ padding: 0.625rem 0.875rem;
+ font-size: 0.8rem;
+ color: var(--text-muted);
+ border-top: 1px solid var(--border);
+}
+
+/* Sortable */
+table.sortable th:not(.sorttable_sorted):not(.sorttable_sorted_reverse):not(.sorttable_nosort)::after {
+ content: " ⇅";
+ color: var(--text-muted);
+ font-size: 0.65rem;
+}
+table.sortable th.sorttable_sorted::after { content: " ↓"; color: var(--color-primary); }
+table.sortable th.sorttable_sorted_reverse::after { content: " ↑"; color: var(--color-primary); }
+
+/* Responsive table */
+@media only screen and (max-width: 800px) {
+ #no-more-tables table,
+ #no-more-tables thead,
+ #no-more-tables tbody,
+ #no-more-tables th,
+ #no-more-tables td,
+ #no-more-tables tr { display: block; }
+ #no-more-tables thead tr { position: absolute; top: -9999px; left: -9999px; }
+ #no-more-tables tr { border: 1px solid var(--border); border-radius: var(--radius-sm); margin-bottom: 0.5rem; }
+ #no-more-tables tbody { padding: 0.5rem; }
+ #no-more-tables td {
+ border: none;
+ border-bottom: 1px solid var(--border);
+ position: relative;
+ padding-left: 50%;
+ white-space: normal;
+ text-align: left;
+ }
+ #no-more-tables td::before {
+ position: absolute;
+ top: 0.625rem; left: 0.625rem;
+ width: 45%;
+ padding-right: 10px;
+ white-space: nowrap;
+ text-align: left;
+ font-weight: 600;
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ content: attr(data-title);
+ }
+}
+
+/* Diff table */
+table.diff { font-weight: 100; line-height: 1.4; }
+table.diff td { padding: 0.1rem; font-family: monospace; font-size: 0.8rem; }
+table.diff td.diff_header { padding-left: 0.5rem; background: var(--bg-muted); color: var(--text-muted); }
+.diff-same-region { display: inline; color: var(--color-primary); }
+.diff-table-td { padding: 3px 10px; font-family: monospace; }
+.diff-div-text { color: #f87171; display: inline; font-family: monospace; }
+.diff_link { cursor: pointer; color: var(--color-primary); }
+
+/* ── 10. Badges / Labels / Tags ──────────────────────────────── */
+label.success { background: rgba(34,197,94,0.12); color: #16a34a; padding: 2px 6px; border-radius: 4px; display: inline-block; }
+label.warning { background: rgba(245,158,11,0.12); color: #d97706; padding: 2px 6px; border-radius: 4px; display: inline-block; }
+label.alert { background: rgba(239,68,68,0.12); color: #dc2626; padding: 2px 6px; border-radius: 4px; display: inline-block; }
+
+.badge {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.2rem 0.55rem;
+ border-radius: 999px;
+ font-size: 0.75rem;
+ font-weight: 600;
+}
+.badge.success { background: rgba(34,197,94,0.12); color: #16a34a; }
+.badge.warning { background: rgba(245,158,11,0.12); color: #d97706; }
+.badge.alert { background: rgba(239,68,68,0.12); color: #dc2626; }
+.badge.info { background: rgba(59,130,246,0.12); color: #2563eb; }
+
+.tag {
+ display: inline-block;
+ background: var(--bg-muted);
+ border: 1px solid var(--border);
+ color: var(--color-primary);
+ padding: 0.2rem 0.6rem;
+ border-radius: var(--radius-sm);
+ font-size: 0.75rem;
+ font-weight: 500;
+ margin-right: 0.25rem;
+ margin-bottom: 0.25rem;
+ transition: background var(--transition), border-color var(--transition);
+}
+.tag:hover { border-color: var(--color-primary); background: var(--color-primary-muted); }
+
+/* ── 11. Progress Tracker ────────────────────────────────────── */
+ol.progtrckr {
+ display: flex;
+ list-style: none;
+ padding: 0;
+ margin: 0 0 2rem;
+ background: var(--bg-surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ padding: 1.5rem 1rem 1rem;
+ box-shadow: var(--shadow-sm);
+}
+ol.progtrckr li {
+ flex: 1;
+ text-align: center;
+ position: relative;
+ font-size: 0.8rem;
+ padding-top: 2.25rem;
+ color: var(--text-muted);
+ font-weight: 500;
+}
+ol.progtrckr li::after {
+ content: '';
+ position: absolute;
+ top: 12px;
+ right: 50%;
+ left: -50%;
+ height: 2px;
+ background: var(--border);
+ z-index: 0;
+}
+ol.progtrckr li:first-child::after { display: none; }
+ol.progtrckr li::before {
+ content: '';
+ position: absolute;
+ top: 0; left: 50%;
+ transform: translateX(-50%);
+ width: 26px; height: 26px;
+ border-radius: 50%;
+ border: 2px solid var(--border);
+ background: var(--bg-surface);
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.7rem;
+ line-height: 26px;
+ text-align: center;
+}
+ol.progtrckr li.progtrckr-done {
+ color: var(--color-success);
+}
+ol.progtrckr li.progtrckr-done::before {
+ content: '✓';
+ background: var(--color-success);
+ border-color: var(--color-success);
+ color: #fff;
+}
+ol.progtrckr li.progtrckr-done::after { background: var(--color-success); opacity: 0.4; }
+ol.progtrckr li.progtrckr-running {
+ color: var(--color-warning);
+}
+ol.progtrckr li.progtrckr-running::before {
+ content: '⏳';
+ background: rgba(245,158,11,0.1);
+ border-color: var(--color-warning);
+}
+ol.progtrckr li.progtrckr-running.error { color: var(--color-danger); }
+ol.progtrckr li.progtrckr-running.error::before {
+ content: '✕';
+ background: var(--color-danger);
+ border-color: var(--color-danger);
+ color: #fff;
+}
+ol.progtrckr li.progtrckr-running.error::after { border-color: var(--color-danger); }
+
+/* ── 12. Category headers (test results) ────────────────────── */
+.category-header { cursor: pointer; user-select: none; }
+.category-header:hover { opacity: 0.8; }
+.category-header.pass { color: var(--color-success); }
+.category-header.fail { color: var(--color-danger); }
+
+/* ── 13. Auth toggle checkbox ────────────────────────────────── */
+.auth_access_ajax { vertical-align: middle; }
+.auth_access_fieldset { position: relative; display: inline-block; }
+
+.toggle {
+ position: absolute;
+ margin-left: -9999px;
+ visibility: hidden;
+}
+.toggle + label {
+ display: block;
+ position: relative;
+ cursor: pointer;
+ outline: none;
+ user-select: none;
+}
+input.toggle-round + label {
+ padding: 2px;
+ width: 56px;
+ height: 28px;
+ background: var(--border);
+ border-radius: 28px;
+ transition: background 0.3s;
+}
+input.toggle-round:checked + label { background: var(--color-primary); }
+input.toggle-round + label::before {
+ display: block;
+ position: absolute;
+ top: 2px; left: 2px; right: 2px; bottom: 2px;
+ content: '';
+ background: rgba(255,255,255,0.2);
+ border-radius: 28px;
+ transition: background 0.3s;
+}
+input.toggle-round + label::after {
+ display: block;
+ position: absolute;
+ top: 3px; left: 3px; bottom: 3px;
+ width: 22px;
+ background: #fff;
+ border-radius: 50%;
+ box-shadow: 0 2px 5px rgba(0,0,0,0.25);
+ transition: left 0.3s;
+ content: '';
+}
+input.toggle-round:checked + label::after { left: calc(100% - 25px); }
+
+/* Fancy checkbox (Font Awesome icons) */
+.hideCheckbox, .hideRadiobutton { display: none; }
+input[type="checkbox"] + label span { display: inline-block; position: relative; }
+input[type="checkbox"] + label span::before {
+ font-family: 'Font Awesome 6 Free';
+ font-weight: 400;
+ content: '\f0c8'; /* fa-square */
+ font-size: 18px;
+ cursor: pointer;
+ color: var(--text-muted);
+}
+input[type="checkbox"]:checked + label span::before {
+ font-weight: 900;
+ content: '\f14a'; /* fa-square-check */
+ color: var(--color-primary);
+}
+input[type="checkbox"]:disabled + label span::before { color: var(--border); cursor: not-allowed; }
+
+/* ── 14. Loader ──────────────────────────────────────────────── */
+.loader {
+ display: inline-block;
+ width: 20px; height: 20px;
+ border: 2.5px solid var(--border);
+ border-top-color: var(--color-primary);
+ border-radius: 50%;
+ animation: spin 0.7s linear infinite;
+}
+@keyframes spin { to { transform: rotate(360deg); } }
+
+/* ── 15. User actions / icon buttons ─────────────────────────── */
+.user-actions i,
+#pageTable th i,
+#serviceTable i,
+#rulesTable i,
+#deployments i,
+#profile_configuration i {
+ display: inline-flex;
+ cursor: pointer;
+ transition: transform var(--transition), color var(--transition);
+}
+.user-actions i:hover { transform: scale(1.15); }
+#pageTable th i { color: var(--color-danger); }
+
+/* ── 16. Flash messages ──────────────────────────────────────── */
+.flashes { list-style: none; padding: 0; margin: 0; }
+.flashes li { padding: 0.25rem 0; }
+
+/* ── 17. Footer ──────────────────────────────────────────────── */
+footer {
+ position: fixed;
+ bottom: 0;
+ left: var(--sidebar-width);
+ right: 0;
+ background: var(--bg-surface);
+ border-top: 1px solid var(--border);
+ padding: 0.5rem 1.5rem;
+ font-size: 0.8rem;
+ color: var(--text-muted);
+ z-index: 50;
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+ transition: left var(--transition);
+}
+footer a { color: var(--text-secondary); text-decoration: none; }
+footer a:hover { color: var(--color-primary); }
+@media (max-width: 900px) {
+ footer { left: 0; }
+}
+
+/* ── 18. Utility classes ─────────────────────────────────────── */
+.hidden { display: none !important; }
+.text-center { text-align: center; }
+.text-right { text-align: right; }
+.text-muted { color: var(--text-muted); }
+.text-success { color: var(--color-success); }
+.text-warning { color: var(--color-warning); }
+.text-danger { color: var(--color-danger); }
+.flex { display: flex; }
+.flex-wrap { flex-wrap: wrap; }
+.items-center { align-items: center; }
+.gap-1 { gap: 0.5rem; }
+.gap-2 { gap: 1rem; }
+.mt-1 { margin-top: 0.5rem; }
+.mt-2 { margin-top: 1rem; }
+.mb-0 { margin-bottom: 0 !important; }
+.mb-1 { margin-bottom: 0.5rem; }
+.mb-2 { margin-bottom: 1rem; }
+.no-bullet { list-style: none; padding-left: 0; }
+.no-bullet li { display: flex; align-items: center; gap: 0.4rem; }
+.no-bullet i { color: var(--color-primary); font-size: 0.875rem; }
+
+/* Foundation compat: .row */
+.flow {
+ background: var(--bg-base);
+ display: flow-root;
+ padding-bottom: 50px;
+}
+
+/* ── 19. Page-specific ───────────────────────────────────────── */
+/* Home hero */
+.home-hero {
+ background: linear-gradient(135deg, var(--color-primary-muted), transparent);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: 2.5rem 2rem;
+ margin-bottom: 2rem;
+}
+.home-stat-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 1rem;
+ margin-bottom: 2rem;
+}
+.stat-card {
+ background: var(--bg-surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ padding: 1.25rem;
+ box-shadow: var(--shadow-sm);
+ display: flex;
+ align-items: flex-start;
+ gap: 1rem;
+ transition: box-shadow var(--transition), transform var(--transition);
+}
+.stat-card:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); }
+.stat-card-icon {
+ width: 42px; height: 42px;
+ border-radius: var(--radius-sm);
+ background: var(--color-primary-muted);
+ color: var(--color-primary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.125rem;
+ flex-shrink: 0;
+}
+.stat-card-body { min-width: 0; }
+.stat-card-label { font-size: 0.75rem; font-weight: 500; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.2rem; }
+.stat-card-value { font-size: 0.9375rem; font-weight: 600; color: var(--text-primary); word-break: break-all; }
+.stat-card-value a { color: var(--color-primary); }
+
+/* ── 20. Microinteractions / Animations ──────────────────────── */
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(6px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+.page-content { animation: fadeIn 0.25s ease; }
+
+/* Focus ring for accessibility */
+:focus-visible {
+ outline: 2px solid var(--color-primary);
+ outline-offset: 2px;
+}
+
+/* ── 21. Print ───────────────────────────────────────────────── */
+@media print {
+ .sidebar, footer, .nav-hamburger { display: none !important; }
+ .page-content { margin-left: 0; }
+}
diff --git a/static/js/app.js b/static/js/app.js
index 3d324c23..533885d9 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -1,152 +1,249 @@
-/*global $, Foundation */
-$(document).foundation();
+/**
+ * CCExtractor CI Platform — Modern Vanilla JS
+ * Replaces jQuery + Foundation JS
+ */
+/* ── Navigation: sidebar toggle (mobile) ───────────────────── */
+(function () {
+ const hamburger = document.getElementById('nav-hamburger');
+ const sidebar = document.getElementById('sidebar');
+ const overlay = document.getElementById('sidebar-overlay');
+ if (!hamburger || !sidebar) return;
+
+ function openSidebar() {
+ sidebar.classList.add('open');
+ if (overlay) overlay.classList.add('open');
+ hamburger.setAttribute('aria-expanded', 'true');
+ const icon = hamburger.querySelector('i');
+ if (icon) { icon.classList.replace('fa-bars', 'fa-xmark'); }
+ }
+ function closeSidebar() {
+ sidebar.classList.remove('open');
+ if (overlay) overlay.classList.remove('open');
+ hamburger.setAttribute('aria-expanded', 'false');
+ const icon = hamburger.querySelector('i');
+ if (icon) { icon.classList.replace('fa-xmark', 'fa-bars'); }
+ }
+
+ hamburger.addEventListener('click', function (e) {
+ e.stopPropagation();
+ sidebar.classList.contains('open') ? closeSidebar() : openSidebar();
+ });
+ if (overlay) overlay.addEventListener('click', closeSidebar);
+
+ // Mobile: tap submenu parent to expand/collapse
+ sidebar.querySelectorAll('.has-submenu > a').forEach(function (link) {
+ link.addEventListener('click', function (e) {
+ if (window.innerWidth <= 900) {
+ e.preventDefault();
+ link.parentElement.classList.toggle('open');
+ }
+ });
+ });
+}());
+
+
+/* ── Theme: dark / light toggle ─────────────────────────────────── */
+(function () {
+ const btn = document.getElementById('theme-toggle');
+ const iconDark = document.getElementById('icon-dark'); // moon — shown in light mode
+ const iconLight = document.getElementById('icon-light'); // sun — shown in dark mode
+ const html = document.documentElement;
+
+ function applyTheme(theme) {
+ html.setAttribute('data-theme', theme);
+ if (iconDark && iconLight) {
+ // Sun shown in light mode, moon shown in dark mode
+ iconLight.style.display = (theme === 'dark') ? 'none' : '';
+ iconDark.style.display = (theme === 'dark') ? '' : 'none';
+ }
+ if (btn) {
+ btn.title = (theme === 'dark') ? 'Switch to light mode' : 'Switch to dark mode';
+ btn.setAttribute('aria-label', btn.title);
+ }
+ }
+
+ // Load saved preference, else respect OS setting
+ const saved = localStorage.getItem('theme');
+ if (saved) {
+ applyTheme(saved);
+ } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ applyTheme('dark');
+ }
+
+ if (btn) {
+ btn.addEventListener('click', function () {
+ const current = html.getAttribute('data-theme');
+ const next = current === 'dark' ? 'light' : 'dark';
+ applyTheme(next);
+ localStorage.setItem('theme', next);
+ });
+ }
+
+ // Sync with OS preference changes (when no manual override)
+ if (window.matchMedia) {
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function (e) {
+ if (!localStorage.getItem('theme')) applyTheme(e.matches ? 'dark' : 'light');
+ });
+ }
+}());
+
+/* ── CIPlatform: error & loader helpers (vanilla JS) ────────────── */
var CIPlatform = {};
+
CIPlatform.errorHandler = (function () {
- 'use strict';
- var instance, createContent, showErrorInElement, showErrorInList,
- showFormErrors, clearFormErrors, showErrorInPopup, registerListeners;
-
- // Init functions
- createContent = function (errors, isFormError) {
- var content, field, idx;
-
- isFormError = isFormError || false;
- content = "";
- if (errors.length === 1) {
- content = errors[0];
- } else {
- content = "The next errors occurred:
Please reload the page to get the current state.
'; + } + + var closeBtn = document.createElement('button'); + closeBtn.innerHTML = '×'; + closeBtn.className = 'button secondary small'; + closeBtn.style.cssText = 'position:absolute;top:1rem;right:1rem;'; + closeBtn.addEventListener('click', function () { document.body.removeChild(overlay); }); + modal.appendChild(closeBtn); + + overlay.appendChild(modal); + overlay.addEventListener('click', function (e) { + if (e.target === overlay) document.body.removeChild(overlay); + }); + document.body.appendChild(overlay); + } + + return { + showErrorInElement : showErrorInElement, + showErrorInList : showErrorInList, + showFormErrors : showFormErrors, + clearFormErrors : clearFormErrors, + showErrorInPopup : showErrorInPopup, + registerListeners : function () {} // kept for backward compatibility + }; }()); + CIPlatform.loadHandler = (function () { - 'use strict'; - var instance, showLoaderInElement, defaultLoaderIcon, defaultLoaderText; - - // Default texts - defaultLoaderIcon = 'fa-cog'; - defaultLoaderText = 'Please wait while we process the request...'; - // Methods - showLoaderInElement = function (jQueryElement, loaderIcon, loaderText) { - loaderIcon = loaderIcon || defaultLoaderIcon; - loaderText = loaderText || defaultLoaderText; - jQueryElement.html(' ' + loaderText); - }; - // Create instance & assign functions - instance = {}; - instance.showLoaderInElement = showLoaderInElement; - - return instance; -}()); + 'use strict'; + var defaultIcon = 'fa-gear'; + var defaultText = 'Please wait while we process the request\u2026'; + + function showLoaderInElement(el, loaderIcon, loaderText) { + loaderIcon = loaderIcon || defaultIcon; + loaderText = loaderText || defaultText; + var domEl = el && el[0] ? el[0] : el; + if (domEl) { + domEl.innerHTML = ' ' + loaderText; + } + } -$(document).ready(function(){ - CIPlatform.errorHandler.registerListeners(); -}); \ No newline at end of file + return { showLoaderInElement: showLoaderInElement }; +}()); \ No newline at end of file diff --git a/static/js/theme.js b/static/js/theme.js index 5a803995..3d89c669 100644 --- a/static/js/theme.js +++ b/static/js/theme.js @@ -1,35 +1,5 @@ -$(document).ready(() => { - $('.theme-toggle i.theme').on('click', toggleTheme); - // Toggle Icon Not Shown Until Page Completely Loads - - // Fetch default theme from localStorage and apply it. - const theme = localStorage.getItem("data-theme"); - if (theme === 'dark') toggleTheme(); - else $('.theme-toggle i.to-dark').removeClass('hidden'); -}); - - -function toggleTheme() { - document.documentElement.toggleAttribute("dark"); - const theme = document.querySelector("#theme-link"); - - const toDark = $('.theme-toggle i.to-dark'); - const toLight = $('.theme-toggle i.to-light'); - // Not Allowing User to Toggle Theme Again Before This Gets Completed. - toDark.addClass('hidden') - toLight.addClass('hidden') - - const lightThemeCSS = "/static/css/foundation-light.min.css"; - const darkThemeCSS = "/static/css/foundation-dark.min.css"; - - if (theme.getAttribute("href") == lightThemeCSS) { - theme.href = darkThemeCSS; - localStorage.setItem("data-theme", "dark"); - toLight.removeClass('hidden') - } - else { - theme.href = lightThemeCSS; - localStorage.setItem("data-theme", "light"); - toDark.removeClass('hidden') - } -} +/** + * theme.js — kept for backward compatibility. + * Theme logic is now handled in app.js via CSS custom properties. + * This file is intentionally empty. + */ diff --git a/templates/base.html b/templates/base.html index c138b307..11d54a22 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,93 +1,88 @@ {%- import 'macros.html' as macros -%} - - - {%- block head %} - - - - -Welcome to the sample submission platform. On this platform you can do the next things:
-Last official release: {{ ccx_last_release.version }} (release date: {{ ccx_last_release.released }}). Regression tests for this version are available here: regression tests
-Latest GitHub commit: {{ ccx_latest_commit }}. View the regression tests.
+ {{ super() }} + + ++ Welcome to the sample submission platform for CCExtractor regression testing. +
+