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:
"; - } - window.console.log(errors); - - return content; - }; - showErrorInElement = function (jQueryElement, errors, fadeOut) { - fadeOut = fadeOut || 0; - jQueryElement.show().html(createContent(errors)); - if (fadeOut > 0) { - setTimeout(function () { jQueryElement.fadeOut(1000); }, fadeOut); - } - }; - showErrorInList = function (jQueryElement, errors) { - jQueryElement.empty(); - errors.forEach(function (elm) { - jQueryElement.append("
  • " + elm + "
  • "); - }); - $("#errorMessage").removeClass("hide"); - }; - showFormErrors = function(jQueryElement, formName, errors, prefix) { - var error, field, form; - - prefix = '' || prefix; - form = document.forms[formName]; - // Show error message - $(form.getElementsByClassName('form-errors')[0]).show(); - // Mark fields - for (error in errors) { - if (errors.hasOwnProperty(error)) { - field = form[prefix + error]; - if(field !== undefined){ - $(field).addClass('is-invalid-input').attr('aria-describedby',field.id + '_error'); - $("#" + field.id + "_error").html(errors[error].join(', ')+'.').addClass('is-visible'); - $('label[for=' + field.id + ']').addClass('is-invalid-label'); - } - } - } - // Clear ajax loader - jQueryElement.html(''); - }; - clearFormErrors = function(formName) { - var form, field, idx; - - form = document.forms[formName]; - $(form.getElementsByClassName('form-errors')[0]).hide(); - // Reset fields - for (idx = 0; idx < form.elements.length; idx++) { - field = form.elements[idx]; - $(field).removeClass('is-invalid-input'); - if($("#"+field.id+"_help_text").length > 0){ - $(field).attr('aria-describedby', field.id + '_help_text'); - } - $("#" + field.id + "_error").html('').removeClass('is-visible'); - if (field.id.length > 0) { - $('label[for=' + field.id + ']').removeClass('is-invalid-label'); - } - } - }; - showErrorInPopup = function(errors, needsPageReload) { - var id, reveal, popup; - - reveal = document.createElement('div'); - id = 'error-popup-'+(new Date()).getTime(); - reveal.setAttribute('id', id); - reveal.setAttribute('class', 'large reveal'); - reveal.setAttribute('data-reveal', ''); - reveal.innerHTML = createContent(errors, true); - if (needsPageReload) { - reveal.innerHTML += 'Please reload the page in order to get the current state for the disabled elements.'; + 'use strict'; + + function createContent(errors, isFormError) { + isFormError = isFormError || false; + if (!Array.isArray(errors) && typeof errors === 'object') { + // form errors object + var parts = []; + for (var f in errors) { + if (Object.prototype.hasOwnProperty.call(errors, f)) { + parts = parts.concat(errors[f]); } - reveal.innerHTML += - ''; - document.body.appendChild(reveal); - popup = new Foundation.Reveal($('#'+id)); - popup.open(); - }; - registerListeners = function () { - // We need to add a listener for foundation reveal close events, so we can unregister the reveal instance. - $(document).on('closed.zf.reveal', function(e){ - $(e.target).foundation('destroy'); - }); - }; - - // Create instance & assign functions - instance = {}; - instance.showErrorInElement = showErrorInElement; - instance.showErrorInList = showErrorInList; - instance.showFormErrors = showFormErrors; - instance.clearFormErrors = clearFormErrors; - instance.showErrorInPopup = showErrorInPopup; - instance.registerListeners = registerListeners; - - return instance; + } + errors = parts; + } + if (errors.length === 1) return errors[0]; + var content = 'The following errors occurred:
    '; + } + + function showErrorInElement(el, errors, fadeOut) { + fadeOut = fadeOut || 0; + // Accept both DOM elements and jQuery-like objects with [0] + var domEl = el && el[0] ? el[0] : el; + if (!domEl) return; + domEl.style.display = ''; + domEl.innerHTML = createContent(errors); + if (fadeOut > 0) { + setTimeout(function () { + domEl.style.transition = 'opacity 1s'; + domEl.style.opacity = '0'; + setTimeout(function () { + domEl.style.display = 'none'; + domEl.style.opacity = ''; + domEl.style.transition = ''; + }, 1000); + }, fadeOut); + } + } + + function showErrorInList(listEl, errors) { + var domEl = listEl && listEl[0] ? listEl[0] : listEl; + if (!domEl) return; + domEl.innerHTML = ''; + errors.forEach(function (e) { + var li = document.createElement('li'); + li.textContent = e; + domEl.appendChild(li); + }); + var errMsg = document.getElementById('errorMessage'); + if (errMsg) errMsg.classList.remove('hidden'); + } + + function showFormErrors(loaderEl, formName, errors, prefix) { + prefix = prefix || ''; + var form = document.forms[formName]; + if (!form) return; + var formErrors = form.getElementsByClassName('form-errors')[0]; + if (formErrors) formErrors.style.display = ''; + for (var error in errors) { + if (!Object.prototype.hasOwnProperty.call(errors, error)) continue; + var field = form[prefix + error]; + if (!field) continue; + field.classList.add('is-invalid-input'); + field.setAttribute('aria-describedby', field.id + '_error'); + var errEl = document.getElementById(field.id + '_error'); + if (errEl) { + errEl.textContent = errors[error].join(', ') + '.'; + errEl.classList.add('is-visible'); + } + var labelEl = form.querySelector('label[for="' + field.id + '"]'); + if (labelEl) labelEl.classList.add('is-invalid-label'); + } + var loaderDom = loaderEl && loaderEl[0] ? loaderEl[0] : loaderEl; + if (loaderDom) loaderDom.innerHTML = ''; + } + + function clearFormErrors(formName) { + var form = document.forms[formName]; + if (!form) return; + var formErrors = form.getElementsByClassName('form-errors')[0]; + if (formErrors) formErrors.style.display = 'none'; + Array.from(form.elements).forEach(function (field) { + field.classList.remove('is-invalid-input'); + var helpText = document.getElementById(field.id + '_help_text'); + if (helpText) field.setAttribute('aria-describedby', field.id + '_help_text'); + var errEl = document.getElementById(field.id + '_error'); + if (errEl) { errEl.textContent = ''; errEl.classList.remove('is-visible'); } + if (field.id) { + var labelEl = form.querySelector('label[for="' + field.id + '"]'); + if (labelEl) labelEl.classList.remove('is-invalid-label'); + } + }); + } + + function showErrorInPopup(errors, needsPageReload) { + var overlay = document.createElement('div'); + overlay.style.cssText = [ + 'position:fixed;inset:0;background:rgba(0,0,0,0.65);z-index:9999;', + 'display:flex;align-items:center;justify-content:center;', + 'animation:fadeIn 0.2s ease;' + ].join(''); + + var modal = document.createElement('div'); + modal.style.cssText = [ + 'background:var(--bg-surface);border-radius:var(--radius-lg);', + 'padding:2rem;max-width:560px;width:90%;position:relative;', + 'box-shadow:var(--shadow-lg);border:1px solid var(--border);' + ].join(''); + modal.innerHTML = createContent(errors, true); + + if (needsPageReload) { + modal.innerHTML += '

    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 %} - - - - - {% block title %}| {{ applicationName }}{% endblock %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {%- endblock %} - - -
    -
    - {%- if hideMenu is not defined -%} - {%- include 'menu.html' -%} - {%- endif -%} -
    -
    - - -
    - {% if hideMenu is not defined %} -
    -
    - - {{ applicationName }} -
    -
    - {% endif %} -
    - {% block body %}{% endblock %} -
    - {% block footer %} - - {% endblock %} -
    -
    -
    - {# Load scripts only at the end of the page #} -
    - {% block scripts %} - - - - - - {% endblock %} -
    - + + + {%- block head %} + + + + {% block title %}| {{ applicationName }}{% endblock %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {%- endblock %} + + + + + + + + + + + + + + {%- if hideMenu is not defined -%} + {%- include 'menu.html' -%} + {%- endif -%} + +
    + {% block body %}{% endblock %} +
    + + {% block footer %} + + {% endblock %} + + {% block scripts %} + + {% endblock %} + diff --git a/templates/home/index.html b/templates/home/index.html index ac70f026..13197fdd 100644 --- a/templates/home/index.html +++ b/templates/home/index.html @@ -1,35 +1,106 @@ {% extends "base.html" %} {% block title %}Home {{ super() }}{% endblock %} + {% block body %} - {{ super() }} -
    -
    -
    -

    {{ applicationName }}

    -

    Welcome to the sample submission platform. On this platform you can do the next things:

    - -

    Development information

    -

    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() }} + + +
    +

    {{ applicationName }}

    +

    + Welcome to the sample submission platform for CCExtractor regression testing. +

    +
      +
    • + + Check regression test status +
    • +
    • + + Upload broken samples +
    • +
    • + + Browse submitted samples +
    • + {% if test_access %} +
    • + + Run your own tests +
    • + {% else %} +
    • + + Run custom tests (requires elevated role) +
    • + {% endif %} +
    +
    + + +
    +
    +
    + +
    +
    +
    Latest Release
    + -
    -
    Helpful links
    - +
    + {{ ccx_last_release.released }} + • + + regression tests +
    +
    +
    + + + +
    +
    + +
    +
    +
    Helpful Links
    + +
    +
    {% endblock %} \ No newline at end of file diff --git a/templates/menu.html b/templates/menu.html index 5b4414e2..2c6d1bce 100644 --- a/templates/menu.html +++ b/templates/menu.html @@ -1,33 +1,50 @@ -{% macro render_entry(data, active_route, level=0) %} - {%- set href = "#" -%} - {%- if 'entries' not in data -%} - {%- if 'route_args' in data -%} - {%- set href = url_for(data.route, **(data.route_args)) -%} - {%- else -%} - {%- set href = url_for(data.route) -%} - {%- endif -%} - {%- endif %} - - 0 %} class="subitem"{% endif %}> - {{ data.title }} - - {%- if 'entries' in data -%} - {%- set level = level + 1 -%} - - {%- endif %} +{% macro render_entry(data, active_route, is_sub=False) %} + {%- set href = "#" -%} + {%- if 'entries' not in data -%} + {%- if 'route_args' in data -%} + {%- set href = url_for(data.route, **(data.route_args)) -%} + {%- else -%} + {%- set href = url_for(data.route) -%} + {%- endif -%} + {%- endif -%} + {%- if 'entries' in data -%} +
  • + + + {{ data.title }} + +
  • + {%- else -%} + + + + {{ data.title }} + + + {%- endif -%} {% endmacro %} -
    - -
    +