{"id":35616365,"url":"https://github.com/mrzoller/mirror-maestro","last_synced_at":"2026-02-16T03:02:06.138Z","repository":{"id":330922103,"uuid":"1120590919","full_name":"MrZoller/mirror-maestro","owner":"MrZoller","description":"Orchestrate GitLab mirrors across multiple instance pairs with precision","archived":false,"fork":false,"pushed_at":"2026-02-09T03:20:37.000Z","size":110563,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-09T03:57:05.664Z","etag":null,"topics":["gitlab"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MrZoller.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-21T14:42:11.000Z","updated_at":"2026-02-09T03:20:39.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/MrZoller/mirror-maestro","commit_stats":null,"previous_names":["mrzoller/mirror-maestro"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/MrZoller/mirror-maestro","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MrZoller%2Fmirror-maestro","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MrZoller%2Fmirror-maestro/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MrZoller%2Fmirror-maestro/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MrZoller%2Fmirror-maestro/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MrZoller","download_url":"https://codeload.github.com/MrZoller/mirror-maestro/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MrZoller%2Fmirror-maestro/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29499528,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-16T02:07:14.481Z","status":"online","status_checked_at":"2026-02-16T02:03:22.852Z","response_time":115,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["gitlab"],"created_at":"2026-01-05T05:15:57.040Z","updated_at":"2026-02-16T03:02:06.130Z","avatar_url":"https://github.com/MrZoller.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Mirror Maestro\n\nOrchestrate GitLab mirrors across multiple instance pairs with precision. A modern web application that streamlines the process of viewing, creating, and maintaining a large set of GitLab mirrors with an intuitive web interface.\n\n![License](https://img.shields.io/badge/license-MIT-blue.svg)\n![Python](https://img.shields.io/badge/python-3.11+-blue.svg)\n![FastAPI](https://img.shields.io/badge/FastAPI-0.115+-green.svg)\n![Docker](https://img.shields.io/badge/docker-ghcr.io-blue.svg)\n\n## Screenshots\n\n\u003cdetails\u003e\n\u003csummary\u003eClick to view screenshots\u003c/summary\u003e\n\n### Dashboard\n![Dashboard](docs/screenshots/01-dashboard.png?v=3)\n*Modern dashboard with live statistics, health charts, recent activity timeline, and quick actions*\n\n### GitLab Instances\n![GitLab Instances](docs/screenshots/02-instances.png?v=3)\n*Manage GitLab instances and rotate their access tokens (tokens are never displayed)*\n\n### Instance Pairs\n![Instance Pairs](docs/screenshots/03-pairs.png?v=3)\n*Configure pairs of GitLab instances for mirroring*\n\n### Mirrors Management\n![Mirrors](docs/screenshots/04-mirrors.png?v=5)\n*View and manage mirrors with pagination, smart path truncation, tree view for nested groups, group filtering, sorting, token status, real-time sync status, safe per-mirror edits, and **issue sync configuration** with visual indicators*\n\n### Issue Mirroring Configuration\n![Issue Configuration](docs/screenshots/04b-issue-config.png?v=1)\n*Configure issue syncing: enable/disable synchronization, choose what to sync (comments, labels, attachments, time tracking), and control behavior including the important **Sync Existing Issues** option (disabled by default - only syncs new issues created after enabling)*\n\n### Topology\n![Topology](docs/screenshots/05-topology.png?v=3)\n*Interactive topology visualization with animated data flows, zoom controls, and hover highlighting - click nodes or links to drill down into mirror details*\n\n### Backup \u0026 Restore\n![Backup](docs/screenshots/06-backup.png?v=4)\n*Complete database backups with one-click creation and secure restore functionality*\n\n### Settings (Multi-User Mode)\n![Settings](docs/screenshots/07-settings.png?v=4)\n*User management with admin and regular user roles, active/inactive status, and secure password management*\n\n### About\n![About](docs/screenshots/08-about.png?v=4)\n*Project information with version details, links to documentation, and technology stack*\n\n### Help\n![Help](docs/screenshots/09-help.png?v=4)\n*Comprehensive help documentation with setup guides, troubleshooting tips, and best practices*\n\n\u003e **Note**: To generate screenshots with sample data, see [docs/screenshots/README.md](docs/screenshots/README.md)\n\n\u003c/details\u003e\n\n## Features\n\n### Core Functionality\n- **Multiple Instance Pairs**: Define and manage mirrors across multiple pairs of GitLab instances (e.g., A↔B, B↔C)\n- **Easy Mirror Creation**: Create mirrors with minimal user input - project information is fetched automatically via the GitLab API\n- **Push \u0026 Pull Mirrors**: Support for both push and pull mirroring configurations\n- **Bidirectional Mirroring**: Create pairs in both directions (A→B and B→A) for two-way sync with independent settings per direction\n- **Automatic Token Management**: Project access tokens are automatically created and managed for each mirror - no manual token configuration needed\n- **Flexible Configuration**: Define default mirror settings at the instance pair level, optionally override per mirror\n- **Safe Inline Editing**: Edit instances/pairs/mirrors in-table; fields that could break existing mirrors are locked/greyed out\n- **Token Rotation**: Rotate instance access tokens or individual mirror tokens without deleting configuration\n- **🆕 Issue Mirroring**: Automatically sync issues, comments, labels, attachments, and PM fields between GitLab instances\n  - One-way sync with bidirectional support via dual mirrors\n  - Configurable sync intervals (5-1440 minutes) with automatic scheduling\n  - Smart change detection using content hashing\n  - PM field conversion (milestones/iterations/epics/assignees → labels)\n  - Attachment download/upload with URL rewriting\n  - Time tracking sync (estimates and time spent)\n  - Loop prevention and incremental syncing\n  - **Production-Ready Robustness**:\n    - Circuit breaker pattern prevents cascading failures\n    - Retry logic with exponential backoff for transient errors\n    - Progress checkpointing for large syncs (resumable on interruption)\n    - Attachment size limits (configurable, default 100MB)\n    - Graceful shutdown waits for active syncs to complete\n    - Batched processing prevents memory exhaustion\n    - Rate limiting prevents API quota exhaustion\n  - See [Issue Mirroring Guide](docs/ISSUE_MIRRORING.md) for details\n\n### Mirror Management\n- **View Mirrors**: See all configured mirrors and their current status at a glance\n- **Pagination \u0026 Scalability**: Handle thousands of mirrors efficiently with paginated views (25/50/100/200 per page)\n- **Smart Path Display**: Nested group paths automatically truncated (e.g., `... / services / api-gateway`) with full path on hover\n- **Tree View**: Hierarchical collapsible view of mirrors grouped by path structure - perfect for navigating deeply nested groups\n- **Advanced Filtering**: Filter mirrors by group path prefix (e.g., `platform/core` shows all mirrors in that tree)\n- **Flexible Sorting**: Sort by created date, updated date, source path, target path, or status - ascending or descending\n- **Create Mirrors**: Quickly set up new mirrors between projects with dropdown selection\n- **Sync Mirrors**: Force immediate mirror synchronization with a single click\n- **Batch Sync**: Sync all mirrors in an instance pair with one click - perfect for resuming after outages\n- **Edit/Remove Mirrors**: Modify safe mirror settings (and revert overrides back to \"inherit\"), or delete mirror configurations as needed\n- **Orphan \u0026 Drift Detection**: Verify mirrors against GitLab to detect external deletions (orphans) or settings changes (drift)\n- **External Mirror Indicator**: See warning badges when selecting projects that already have mirrors configured on GitLab\n- **Import/Export**: Bulk import and export mirror settings with automatic rate limiting for large operations\n- **Backup \u0026 Restore**: Create complete backups of your database and encryption key; restore from backups to recover or migrate\n- **Production-Ready Robustness**: All GitLab API operations use enterprise-grade patterns:\n  - Rate limiting prevents API quota exhaustion\n  - Retry logic with exponential backoff for transient errors\n  - Circuit breakers (per-instance) prevent cascading failures\n\n### Modern Web Interface\n- **Comprehensive Dashboard**: Live statistics cards, health distribution charts (Chart.js), recent activity timeline, and quick actions\n- **Dark Mode**: Beautiful dark theme with smooth transitions and localStorage persistence - toggle anytime with the sun/moon button\n- **Live Status Polling**: Real-time updates every 30 seconds with pulsing indicators for actively syncing mirrors\n- **Enhanced Topology**: Animated particle system showing data flow, zoom controls (+/−/reset), and smart hover highlighting\n- **Clean, Responsive Design**: Modern card-based layout with smooth animations and tabbed navigation\n- **Intuitive Workflow**: Straightforward mirror management with visual feedback and status indicators\n- Similar look and feel to [issue-bridge](https://github.com/MrZoller/issue-bridge)\n\n## Architecture\n\n### Technology Stack\n- **Backend**: Python 3.11+ with FastAPI\n- **Database**: PostgreSQL (async with asyncpg)\n- **Frontend**: Vanilla JavaScript with modern CSS\n- **Visualization**: Chart.js for charts, D3.js for topology graphs\n- **API Integration**: python-gitlab library\n- **Deployment**: Docker and Docker Compose\n- **Authentication**: HTTP Basic Auth (single-user) or JWT tokens (multi-user)\n- **Security**: Encrypted storage of GitLab tokens using Fernet encryption\n\n### Project Structure\n```\nmirror-maestro/\n├── app/\n│   ├── api/              # API route handlers\n│   │   ├── dashboard.py  # Dashboard metrics\n│   │   ├── instances.py  # GitLab instance management\n│   │   ├── pairs.py      # Instance pair management\n│   │   ├── mirrors.py    # Mirror CRUD operations\n│   │   ├── topology.py   # Topology visualization\n│   │   └── export.py     # Import/export functionality\n│   ├── core/             # Core functionality\n│   │   ├── auth.py       # Authentication\n│   │   ├── encryption.py # Token encryption\n│   │   ├── gitlab_client.py # GitLab API wrapper\n│   │   └── rate_limiter.py # Rate limiting for batch operations\n│   ├── static/           # Frontend assets\n│   │   ├── css/          # Modern CSS with design tokens\n│   │   └── js/           # Vanilla JS with D3.js \u0026 Chart.js\n│   ├── templates/        # HTML templates\n│   ├── config.py         # Application configuration\n│   ├── database.py       # Database setup\n│   ├── models.py         # SQLAlchemy models\n│   └── main.py           # FastAPI application\n├── data/                 # Database and encryption keys\n├── docker-compose.yml    # Docker Compose configuration\n├── Dockerfile            # Docker image definition\n├── requirements.txt      # Python dependencies\n└── README.md             # This file\n```\n\n## Quick Start\n\n### Using Published Docker Image (Recommended)\n\nThe easiest way to run Mirror Maestro is using the pre-built Docker image from GitHub Container Registry:\n\n1. **Pull the image**\n   ```bash\n   docker pull ghcr.io/mrzoller/mirror-maestro:latest\n   ```\n\n2. **Create configuration files**\n   ```bash\n   # Create directories\n   mkdir -p mirror-maestro \u0026\u0026 cd mirror-maestro\n   mkdir -p data ssl nginx\n\n   # Download docker-compose and example config\n   curl -O https://raw.githubusercontent.com/MrZoller/mirror-maestro/main/docker-compose.yml\n   curl -O https://raw.githubusercontent.com/MrZoller/mirror-maestro/main/.env.example\n   curl -o nginx/nginx.conf https://raw.githubusercontent.com/MrZoller/mirror-maestro/main/nginx/templates/default.conf.template\n\n   # Configure environment\n   cp .env.example .env\n   # Edit .env with your preferred settings\n   ```\n\n3. **Create docker-compose.override.yml** to use the published image:\n   ```yaml\n   version: '3.8'\n   services:\n     app:\n       image: ghcr.io/mrzoller/mirror-maestro:latest\n   ```\n\n4. **Start the application**\n   ```bash\n   docker-compose up -d\n   ```\n\n5. **Access the web interface**\n   Open your browser to `http://localhost` (or `http://localhost:8000` if not using nginx)\n\n   Default credentials (if auth is enabled):\n   - Username: `admin`\n   - Password: `changeme`\n\n### Using Docker with Local Build\n\nIf you prefer to build the image locally:\n\n1. **Clone the repository**\n   ```bash\n   git clone https://github.com/MrZoller/mirror-maestro.git\n   cd mirror-maestro\n   ```\n\n2. **Configure environment**\n   ```bash\n   cp .env.example .env\n   # Edit .env with your preferred settings\n   ```\n\n3. **Start the application**\n   ```bash\n   docker-compose up -d\n   ```\n\n4. **Access the web interface**\n   Open your browser to `http://localhost`\n\n   Default credentials (if auth is enabled):\n   - Username: `admin`\n   - Password: `changeme`\n\n### Local Development\n\n1. **Install dependencies**\n   ```bash\n   python -m venv venv\n   source venv/bin/activate  # On Windows: venv\\Scripts\\activate\n   pip install -r requirements.txt\n   ```\n\n2. **Create data directory**\n   ```bash\n   mkdir -p data\n   ```\n\n3. **Configure environment**\n   ```bash\n   cp .env.example .env\n   # Edit .env as needed\n   ```\n\n4. **Run the application**\n   ```bash\n   python -m app.main\n   # Or use uvicorn directly:\n   uvicorn app.main:app --reload\n   ```\n\n5. **Access the web interface**\n   Open your browser to `http://localhost:8000`\n\n## Configuration\n\n### Environment Variables\n\nCreate a `.env` file with the following variables:\n\n```bash\n# Environment Mode (development, staging, production)\n# IMPORTANT: Set to 'production' for production deployments\n# In production mode, default credentials are rejected at startup\nENVIRONMENT=development\n\n# Server Configuration\nHOST=0.0.0.0\nPORT=8000\n\n# Database Configuration (PostgreSQL)\nDATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/mirror_maestro\n\n# PostgreSQL credentials (used by docker-compose)\nPOSTGRES_USER=postgres\nPOSTGRES_PASSWORD=postgres\nPOSTGRES_DB=mirror_maestro\n\n# Authentication (optional but recommended)\nAUTH_ENABLED=true\nAUTH_USERNAME=admin\nAUTH_PASSWORD=changeme\n\n# Multi-User Mode (optional)\n# Set to true to enable JWT-based authentication with multiple users\nMULTI_USER_ENABLED=false\n# Initial admin user (only used when multi-user mode is first enabled)\nINITIAL_ADMIN_USERNAME=admin\nINITIAL_ADMIN_PASSWORD=changeme\nINITIAL_ADMIN_EMAIL=\n# JWT Settings (auto-generated secret if not provided)\nJWT_SECRET_KEY=your-secret-key-here\nJWT_ALGORITHM=HS256\nJWT_EXPIRATION_HOURS=24\n\n# Logging\nLOG_LEVEL=INFO\n\n# Application Settings\nAPP_TITLE=Mirror Maestro\nAPP_DESCRIPTION=Orchestrate GitLab mirrors across multiple instance pairs with precision\n\n# SSL/TLS Configuration\nSSL_ENABLED=false\nSSL_CERT_PATH=/etc/nginx/ssl/cert.pem\nSSL_KEY_PATH=/etc/nginx/ssl/key.pem\n\n# Optional: Customize ports\nHTTP_PORT=80\nHTTPS_PORT=443\n\n# Rate Limiting (for batch operations and imports)\n# Delay between GitLab API operations to avoid overwhelming instances\nGITLAB_API_DELAY_MS=200  # 200ms = ~300 ops/min (well under 600/min limit)\nGITLAB_API_MAX_RETRIES=3  # Retries on rate limit errors\n```\n\n### GitLab Access Tokens\n\nThe app uses **Instance Access Tokens** to call the GitLab API:\n\n- **Instance Access Token** (stored per GitLab instance)\n  - Required scope: `api` (needed to manage mirrors and create project access tokens)\n  - You can **rotate** this token from the **GitLab Instances** table (Edit → paste new token → Save)\n\n**Automatic Mirror Tokens**: When you create a mirror, the app automatically creates a project access token on the appropriate project (source for pull mirrors, target for push mirrors). These tokens are managed automatically:\n- Created when you create a mirror\n- Deleted when you delete a mirror\n- Can be manually rotated via the \"Rotate Token\" button in the mirrors table\n\n### Multi-User Mode\n\nMirror Maestro supports two authentication modes:\n\n#### Single-User Mode (Default)\n- Uses HTTP Basic Auth with a single username/password\n- Configured via `AUTH_ENABLED`, `AUTH_USERNAME`, `AUTH_PASSWORD`\n- Good for personal use or small teams\n\n#### Multi-User Mode\n- JWT-based authentication with individual user accounts\n- Admin users can create and manage other users\n- Each user has their own login credentials\n- Enabled by setting `MULTI_USER_ENABLED=true`\n\n**Enabling Multi-User Mode:**\n\n1. Set the following environment variables:\n   ```bash\n   MULTI_USER_ENABLED=true\n   INITIAL_ADMIN_USERNAME=admin\n   INITIAL_ADMIN_PASSWORD=your-secure-password\n   JWT_SECRET_KEY=your-jwt-secret-key\n   ```\n\n2. Restart the application. An initial admin user will be created automatically.\n\n3. Log in with the admin credentials and go to the **Settings** tab to manage users.\n\n**User Management (Admin only):**\n- Create new users with username, email (optional), and password\n- Assign admin privileges to users\n- Deactivate users without deleting them\n- Delete users (except yourself and the last admin)\n\n**User Features:**\n- Change your own password via the user menu\n- View your username and role in the top-right user menu\n\n### SSL/TLS Configuration\n\nMirror Maestro supports optional SSL/TLS encryption for secure HTTPS connections. This is handled by an nginx reverse proxy that sits in front of the FastAPI application.\n\n#### Quick Start with Self-Signed Certificate (Development)\n\nFor testing and development environments, you can quickly generate a self-signed certificate:\n\n```bash\n# 1. Generate self-signed certificate\n./scripts/generate-self-signed-cert.sh\n\n# 2. Enable SSL in your .env file\necho \"SSL_ENABLED=true\" \u003e\u003e .env\n\n# 3. Configure nginx\n./scripts/setup-ssl.sh\n\n# 4. Start the application\ndocker-compose up -d\n\n# 5. Access via HTTPS\n# Open https://localhost (your browser will warn about the self-signed cert)\n```\n\n**Note:** Self-signed certificates will trigger browser security warnings. They are suitable for development only.\n\n#### Production Setup with Valid Certificates\n\nFor production deployments, use certificates from a trusted Certificate Authority (CA) like Let's Encrypt:\n\n1. **Obtain SSL certificates** from your CA (e.g., using certbot for Let's Encrypt)\n\n2. **Copy certificates to the ssl directory:**\n   ```bash\n   mkdir -p ssl\n   cp /path/to/your/fullchain.pem ssl/cert.pem\n   cp /path/to/your/privkey.pem ssl/key.pem\n   ```\n\n3. **Enable SSL in your .env file:**\n   ```bash\n   SSL_ENABLED=true\n   SSL_CERT_PATH=/etc/nginx/ssl/cert.pem\n   SSL_KEY_PATH=/etc/nginx/ssl/key.pem\n   ```\n\n4. **Configure nginx:**\n   ```bash\n   ./scripts/setup-ssl.sh\n   ```\n\n5. **Optional: Customize ports** in your .env file:\n   ```bash\n   HTTP_PORT=80      # HTTP port (redirects to HTTPS when SSL is enabled)\n   HTTPS_PORT=443    # HTTPS port\n   ```\n\n6. **Start the application:**\n   ```bash\n   docker-compose up -d\n   ```\n\n#### SSL Configuration Details\n\nWhen SSL is enabled (`SSL_ENABLED=true`):\n- HTTP requests on port 80 are automatically redirected to HTTPS on port 443\n- The nginx reverse proxy handles SSL termination\n- Modern TLS protocols (TLSv1.2, TLSv1.3) and secure cipher suites are used\n- Security headers are automatically added (HSTS, X-Frame-Options, etc.)\n\nWhen SSL is disabled (`SSL_ENABLED=false`):\n- The application is served over HTTP only\n- No SSL certificates are required\n- Suitable for development or when SSL is handled by external infrastructure (load balancer, reverse proxy, etc.)\n\n#### Certificate Renewal\n\nFor production certificates that expire (e.g., Let's Encrypt certificates expire every 90 days):\n\n1. Renew your certificates using your CA's renewal process\n2. Copy the new certificates to the `ssl/` directory (same filenames)\n3. Reload nginx: `docker-compose restart nginx`\n\nNo need to restart the entire application stack.\n\n#### Troubleshooting SSL\n\n**\"SSL certificates not found\" error:**\n- Ensure `ssl/cert.pem` and `ssl/key.pem` exist\n- Check file permissions (cert should be readable, key should be 600)\n- Run `./scripts/generate-self-signed-cert.sh` for development\n\n**Browser shows \"connection not secure\":**\n- Normal for self-signed certificates\n- Click \"Advanced\" → \"Proceed anyway\" for testing\n- For production, use certificates from a trusted CA\n\n**nginx fails to start:**\n- Check nginx logs: `docker-compose logs nginx`\n- Verify certificate files are valid: `openssl x509 -in ssl/cert.pem -text -noout`\n- Ensure ports 80 and 443 are not already in use\n\n### Production Deployment\n\nMirror Maestro includes several production-ready features for secure, reliable deployments.\n\n#### Production Environment Mode\n\nSet `ENVIRONMENT=production` to enable strict security validation:\n\n```bash\n# In .env\nENVIRONMENT=production\nAUTH_PASSWORD=your-secure-password-here  # Required - 'changeme' is rejected\nPOSTGRES_PASSWORD=your-secure-db-password  # Required - 'postgres' is rejected\n```\n\n**In production mode, the application will refuse to start if:**\n- `AUTH_PASSWORD` is still set to `changeme`\n- `INITIAL_ADMIN_PASSWORD` is still set to `changeme` (when multi-user mode is enabled)\n- `DATABASE_URL` contains default credentials (`postgres:postgres`)\n\nThis prevents accidental deployment with insecure defaults.\n\n#### Security Features\n\n**Security Headers**: All responses include security headers:\n- `X-Frame-Options: DENY` - Prevents clickjacking\n- `X-Content-Type-Options: nosniff` - Prevents MIME sniffing\n- `X-XSS-Protection: 1; mode=block` - XSS filter\n- `Referrer-Policy: strict-origin-when-cross-origin`\n- `Permissions-Policy` - Restricts browser features\n\n**HTTP Rate Limiting**: Built-in protection against brute force attacks:\n- Auth endpoints: 5 requests/minute\n- Write operations: 30 requests/minute\n- Read operations: 100 requests/minute\n- Sync operations: 10 requests/minute\n\n**Docker Security**: The container runs as non-root user `appuser` (UID 1000) for improved security.\n\n**Encryption Key Permissions**: The encryption key file is automatically set to `0o600` (owner read/write only).\n\n#### Resource Limits\n\nThe `docker-compose.yml` includes CPU and memory limits for all services:\n\n| Service | CPU Limit | Memory Limit | Memory Reservation |\n|---------|-----------|--------------|-------------------|\n| Database (PostgreSQL) | 2 | 1GB | 256MB |\n| Application (FastAPI) | 2 | 2GB | 512MB |\n| Nginx | 1 | 256MB | 64MB |\n\nAdjust these in `docker-compose.yml` based on your workload.\n\n#### Request Logging\n\nAll requests are logged with:\n- Correlation IDs (`X-Request-ID` header for tracing)\n- Request method and path\n- Response status code\n- Request duration\n\nExample log output:\n```\n2026-01-03 12:34:56 [INFO] [abc12345] GET /api/mirrors -\u003e 200 (45ms)\n```\n\n#### Database Migrations\n\nMirror Maestro uses [Alembic](https://alembic.sqlalchemy.org/) for database schema management:\n\n```bash\n# Create a new migration\nalembic revision --autogenerate -m \"description of change\"\n\n# Apply migrations\nalembic upgrade head\n\n# View current revision\nalembic current\n\n# View migration history\nalembic history\n```\n\nFor Docker deployments, migrations can be run in the container:\n```bash\ndocker-compose exec app alembic upgrade head\n```\n\n#### Monitoring Recommendations\n\nFor production monitoring, consider:\n- **Health check endpoint**: `GET /api/health/quick` (no auth required, suitable for load balancers)\n- **Detailed health**: `GET /api/health` (authenticated, includes component status)\n- **Logs**: Configure log aggregation for the `app` container\n- **Metrics**: Monitor request latency, error rates, and sync job status\n\n#### Enterprise Deployment with Local Artifact Mirrors\n\n**New in v1.2.0**: Mirror Maestro supports deployment in air-gapped or enterprise environments where external internet access is restricted.\n\nAll dependencies can be pulled from local mirrors (Nexus, Artifactory, Harbor) instead of public repositories:\n\n- **Docker Images**: Redirect to private Docker registry\n- **APT Packages**: Use internal Ubuntu mirror\n- **Python Packages**: Use internal PyPI mirror\n- **Frontend Assets**: Use local copies or custom CDN\n\n**Quick Configuration**:\n\n```bash\n# In .env file\nDOCKER_REGISTRY=harbor.company.com/proxy/\nAPT_MIRROR=http://nexus.company.com/repository/ubuntu-proxy/ubuntu\nPIP_INDEX_URL=http://nexus.company.com/repository/pypi-proxy/simple\nPIP_TRUSTED_HOST=nexus.company.com\nUSE_LOCAL_VENDOR_ASSETS=true  # Use local Chart.js/D3.js copies\n```\n\n**For air-gapped deployments**, download frontend vendor assets:\n\n```bash\n./scripts/download-vendor-assets.sh  # Run on internet-connected machine\n# Copy app/static/vendor/ directory to your deployment\n```\n\n**Complete Documentation**: See [docs/ENTERPRISE_DEPLOYMENT.md](docs/ENTERPRISE_DEPLOYMENT.md) for:\n- Detailed Nexus/Artifactory configuration examples\n- Step-by-step deployment guide\n- Troubleshooting common issues\n- SSL/TLS certificate handling for internal mirrors\n\n## Usage Guide\n\n### 1. Add GitLab Instances\n\nFirst, configure the GitLab instances you want to mirror between:\n\n1. Go to the **GitLab Instances** tab\n2. Fill the **Add Instance** form\n3. Provide:\n   - Name (e.g., \"Production GitLab\")\n   - URL (e.g., \"https://gitlab.example.com\")\n   - Access Token (Personal or Group Access Token)\n   - Description (optional)\n\n#### Rotating instance access tokens\nYou can rotate the stored instance access token without changing the instance URL:\n- Click **Edit** on an instance row\n- Paste a new **Access Token**\n- Click **Save**\n\n\u003e The token value is **never displayed** in the UI (only a masked placeholder is shown).\n\n#### Deletion behavior (important)\nTo prevent broken configurations, the app performs **cascading deletes with GitLab cleanup**:\n\n- Deleting a **GitLab instance** also deletes any **instance pairs** that reference it and any **mirrors** belonging to those pairs.\n  - **GitLab cleanup**: Before database deletion, the app removes all mirrors and project access tokens from GitLab using rate-limited API calls\n  - **Rate limiting**: For instances with many mirrors, cleanup operations are throttled to avoid overwhelming GitLab (configurable delay between operations)\n  - **Best-effort**: If GitLab cleanup fails for some mirrors (e.g., network errors, token expired), the database deletion still proceeds, and warnings are returned\n\n- Deleting an **instance pair** also deletes any **mirrors** belonging to that pair.\n  - **GitLab cleanup**: Removes all mirrors and tokens from GitLab before database deletion\n  - **Rate limiting**: Applied when deleting pairs with multiple mirrors\n  - **Progress tracking**: The operation returns detailed metrics including success/failure counts and operation duration\n\n- Deleting a **mirror** also deletes its automatically-created project access token from GitLab.\n  - **Best-effort**: If token deletion fails, the mirror is still removed from the database with a warning\n\n**Rate Limiting Configuration:**\n- Default: 200ms delay between GitLab API operations (~300 operations/minute)\n- Configurable via `GITLAB_API_DELAY_MS` environment variable\n- Automatic retry with exponential backoff on rate limit errors (429 responses)\n\nThe UI shows a warning and requires confirmation before performing these actions.\n\n### 2. Create Instance Pairs\n\nDefine pairs of instances where mirrors will be created:\n\n1. Go to the **Instance Pairs** tab\n2. Click **Create Pair**\n3. Configure:\n   - Pair name (e.g., \"Prod to Backup\")\n   - Source instance\n   - Target instance\n   - Mirror direction (push or pull)\n   - Default mirror settings:\n     - Mirror protected branches\n     - Overwrite divergent branches\n     - Trigger builds on update\n     - Only mirror protected branches\n\n### 3. Manage Mirrors\n\nCreate and manage mirrors between projects:\n\n1. Go to the **Mirrors** tab\n2. Select an instance pair from the dropdown\n3. To create a new mirror:\n   - Select source project (auto-populated from GitLab)\n   - Select target project (auto-populated from GitLab)\n   - Click **Create Mirror**\n4. To manage existing mirrors:\n   - **Edit**: Update safe per-mirror overrides (and optionally clear them back to \"inherit\")\n   - **Sync**: Force an immediate mirror synchronization\n   - **Rotate Token**: Create a new access token for the mirror (revokes the old one)\n   - **Delete**: Remove the mirror configuration (also deletes the access token)\n\n### 4. Batch Mirror Sync\n\nSync all mirrors in an instance pair with one click - particularly useful after GitLab outages or maintenance.\n\n#### When to Use Batch Sync\n\n- **After outages**: When a GitLab instance goes down temporarily, all mirrors may stop syncing\n- **Post-maintenance**: After scheduled maintenance or upgrades\n- **Large-scale updates**: When you need to sync hundreds of mirrors at once\n\n#### How to Use\n\n1. Go to the **Instance Pairs** tab\n2. Click the **Sync All** button for the desired pair\n3. Confirm the operation (shows mirror count and estimated duration)\n4. View detailed results with success/failure counts and timing metrics\n\n#### Rate Limiting Protection\n\nTo prevent overwhelming GitLab instances with too many API requests, batch sync uses intelligent rate limiting:\n\n- **Configurable delay**: Default 200ms between operations (~300 requests/minute, well under GitLab's typical 600/min limit)\n- **Automatic retry**: If GitLab returns a 429 \"Too Many Requests\" error, operations are retried with exponential backoff\n- **Progress tracking**: Real-time progress with detailed reporting\n- **Continue on failure**: Processing continues even if some mirrors fail\n\n**Example**: Syncing 100 mirrors with default settings takes ~20 seconds and processes at a safe rate of ~300 operations/minute.\n\n### 5. Import/Export\n\nBulk manage mirror configurations with portable JSON files.\n\n#### How to Use\n\n1. **Select an instance pair** from the Mirrors tab\n2. Click **Export** to download all mirrors for that pair as JSON\n3. Click **Import** to upload a JSON file and create mirrors for the selected pair\n\n#### Export Format\n\nExports are **portable across environments** (dev/staging/prod):\n\n- **Project paths** (not IDs) - e.g., `group/subgroup/project`\n- **Mirror settings** - All configuration options (overwrite diverged, protected branches, etc.)\n- **Metadata** (informational only) - Source instance, target instance, direction, export timestamp\n\nExample export structure:\n```json\n{\n  \"metadata\": {\n    \"exported_at\": \"2024-01-15T10:30:00Z\",\n    \"pair_name\": \"GitLab.com → Self-hosted\",\n    \"source_instance_name\": \"GitLab.com\",\n    \"source_instance_url\": \"https://gitlab.com\",\n    \"target_instance_name\": \"Self-hosted\",\n    \"target_instance_url\": \"https://gitlab.example.com\",\n    \"mirror_direction\": \"push\",\n    \"total_mirrors\": 2\n  },\n  \"mirrors\": [\n    {\n      \"source_project_path\": \"mygroup/project1\",\n      \"target_project_path\": \"mirrors/project1\",\n      \"mirror_overwrite_diverged\": false,\n      \"only_mirror_protected_branches\": true,\n      \"enabled\": true\n    }\n  ]\n}\n```\n\n#### Import Process\n\nWhen you import mirrors, Mirror Maestro:\n\n1. **Looks up project IDs** from paths via GitLab API (2 API calls per mirror)\n2. **Creates project access tokens** in GitLab (1 API call per mirror)\n3. **Creates actual mirrors** in GitLab - push or pull (1 API call per mirror)\n4. **Stores mirror records** in the database\n5. **Applies rate limiting** - Waits 200ms before processing the next mirror\n\nThe result is **identical to creating mirrors via the UI**.\n\n**Rate Limiting**: Each mirror import requires ~4 GitLab API calls. With default settings (200ms delay), importing 100 mirrors takes approximately 40-60 seconds and processes at a safe rate of ~200-300 API requests/minute. This prevents overwhelming your GitLab instances while ensuring reliable imports.\n\n#### Import Results\n\nAfter import completes, you'll see a detailed summary:\n\n- **Imported count** - Successfully created mirrors\n- **Skipped count** - Mirrors that already exist\n- **Errors** - Detailed list of failures with specific project paths\n- **Skipped details** - Which mirrors were skipped and why\n\nExample:\n```\nImport complete: 8 imported, 2 skipped\n\nSkipped (2):\n  • [1/10] group/existing → mirror/existing: Already exists in database\n  • [5/10] group/duplicate → mirror/duplicate: Already exists in database\n```\n\n#### Important Notes\n\n- **Select the correct pair** before importing - the import creates mirrors for the currently selected pair\n- **Metadata is ignored** on import - only the `mirrors` array is used\n- **Projects must exist** - Both source and target projects must exist in their respective GitLab instances\n- **Duplicates are skipped** - If a mirror already exists (same source/target paths), it won't be created again\n- **Import continues on errors** - If some mirrors fail, others will still be imported\n\n### 5. Backup \u0026 Restore\n\nProtect your Mirror Maestro configuration with complete database backups:\n\n1. Go to the **Backup** tab\n2. **Creating a Backup**:\n   - Review current statistics (instances, pairs, mirrors, database size)\n   - Click **Create \u0026 Download Backup**\n   - Save the `.tar.gz` file in a secure location\n3. **Restoring from a Backup**:\n   - Click **Select Backup File** and choose your backup archive\n   - Optionally enable \"Create backup before restore\" (recommended)\n   - Click **Restore Backup** and confirm the action\n   - The application will reload with the restored data\n\n#### Backup Contents\n\nEach backup archive includes:\n- **Database export** - All GitLab instances, instance pairs, and mirrors (JSON format)\n- **Encryption key** - Required to decrypt stored GitLab tokens\n- **Metadata** - Backup timestamp, version, and file manifest\n\n#### Security Warning\n\n⚠️ **Important**: Backup files contain your encryption key and can decrypt all stored GitLab tokens. Always:\n- Store backups in a secure location\n- Use encryption or access controls on backup storage\n- Never share backups publicly\n- Keep backups separate from your Mirror Maestro server\n\n#### Best Practices\n\n- **Regular backups**: Create backups daily or weekly depending on change frequency\n- **Test restores**: Periodically verify backups can be restored successfully\n- **Version retention**: Keep multiple backup versions in case of corruption\n- **Migration**: Use backups to migrate between servers or Docker hosts\n- **Pre-restore safety**: Always enable \"Create backup before restore\" when restoring\n\n#### Backup Format\n\nBackups are compressed tar archives (`.tar.gz`) with the naming format:\n```\nmirror-maestro-backup-YYYYMMDD-HHMMSS.tar.gz\n```\n\nArchives are portable across Mirror Maestro versions and can be restored on any compatible server.\n\n## API Documentation\n\nThe application provides a RESTful API. Once running, visit:\n- Swagger UI: `http://localhost:8000/docs`\n- ReDoc: `http://localhost:8000/redoc`\n\n### Key Endpoints\n\n#### GitLab Instances\n- `GET /api/instances` - List all instances\n- `POST /api/instances` - Create new instance\n- `GET /api/instances/{id}` - Get instance details\n- `PUT /api/instances/{id}` - Update instance\n- `DELETE /api/instances/{id}` - Delete instance\n- `GET /api/instances/{id}/projects` - Get projects for instance\n- `GET /api/instances/{id}/projects/{project_id}/mirrors` - Get existing mirrors for a project\n\n#### Instance Pairs\n- `GET /api/pairs` - List all pairs\n- `POST /api/pairs` - Create new pair\n- `GET /api/pairs/{id}` - Get pair details\n- `PUT /api/pairs/{id}` - Update pair\n- `DELETE /api/pairs/{id}` - Delete pair\n- `POST /api/pairs/{id}/sync-mirrors` - Batch sync all enabled mirrors in a pair (with rate limiting)\n\n#### Mirrors\n- `GET /api/mirrors` - List all mirrors\n- `POST /api/mirrors` - Create new mirror\n- `GET /api/mirrors/{id}` - Get mirror details\n- `PUT /api/mirrors/{id}` - Update mirror\n- `DELETE /api/mirrors/{id}` - Delete mirror\n- `POST /api/mirrors/{id}/update` - Trigger mirror update\n- `POST /api/mirrors/{id}/rotate-token` - Rotate the mirror's access token\n- `GET /api/mirrors/{id}/verify` - Verify mirror for orphan/drift status\n- `POST /api/mirrors/verify` - Batch verify multiple mirrors\n\n#### Import/Export\n- `GET /api/export/pair/{id}` - Export mirrors for a pair\n- `POST /api/export/pair/{id}` - Import mirrors for a pair\n\n#### Topology\n- `GET /api/topology` - Aggregated instance/link graph (supports staleness thresholds and \"never succeeded\" handling)\n- `GET /api/topology/link-mirrors` - Drill down: list mirrors behind a topology link\n\n#### Dashboard\n- `GET /api/dashboard/metrics` - Dashboard metrics and statistics (total mirrors, health %, recent activity, charts)\n- `GET /api/dashboard/quick-stats` - Quick stats for live polling (syncing count, recent failures)\n\n#### Backup \u0026 Restore\n- `GET /api/backup/stats` - Get backup statistics (instance/pair/mirror counts, database size)\n- `GET /api/backup/create` - Create and download a complete backup archive\n- `POST /api/backup/restore` - Restore from a backup archive (multipart form upload)\n\n#### Health Check\n- `GET /api/health/quick` - Quick health check for load balancers (no auth required)\n- `GET /api/health` - Detailed health check with component status, mirror stats, and token expiration\n- `GET /api/health?check_instances=true` - Extended health check with GitLab instance connectivity tests\n- `GET /health` - Legacy health endpoint for backward compatibility\n\n#### Search\n- `GET /api/search?q={query}` - Global search across instances, pairs, and mirrors\n\n## Security\n\n### Token Encryption\nAll GitLab access tokens (instance tokens and mirror tokens) are encrypted using Fernet (symmetric encryption) before being stored in the database. The encryption key is automatically generated and stored in `data/encryption.key`.\n\n**Important**: Keep the `data/encryption.key` file secure and backed up. Without it, you won't be able to decrypt stored tokens.\n\n### Automatic Mirror Token Management\nMirrors use automatically-created project access tokens for HTTPS authentication. When creating a mirror:\n\n1. The app determines which project needs the token:\n   - **Pull mirrors**: Token is created on the source project (allows reading from it)\n   - **Push mirrors**: Token is created on the target project (allows pushing to it)\n\n2. A project access token is created with appropriate scopes:\n   - `read_repository` for pull mirrors\n   - `write_repository` for push mirrors\n\n3. The token is used to construct an authenticated URL: `https://token_name:token@gitlab.example.com/project.git`\n\n4. The token is encrypted and stored with the mirror record\n\n**Token Lifecycle**:\n- Tokens are automatically created when mirrors are created\n- Tokens are automatically deleted when mirrors are deleted\n- Tokens can be manually rotated via the \"Rotate Token\" button (creates new token, revokes old one)\n- Tokens expire after 1 year by default\n\n### Authentication\nHTTP Basic Authentication can be enabled to protect the web interface. Configure credentials in the `.env` file:\n\n```bash\nAUTH_ENABLED=true\nAUTH_USERNAME=your_username\nAUTH_PASSWORD=your_secure_password\n```\n\n### Best Practices\n1. Always use HTTPS in production\n2. Use instance tokens with appropriate `api` scope for GitLab API access\n3. Monitor token expiration - mirror tokens expire after 1 year\n4. Use the \"Rotate Token\" feature when tokens are about to expire\n5. Keep the encryption key secure\n6. Use strong passwords for HTTP Basic Auth\n7. Restrict network access to the application\n8. Regularly backup the database and encryption key\n\n## Troubleshooting\n\n### Common Issues\n\n**Connection Refused**\n- Ensure the GitLab URL is correct and accessible\n- Verify the access token has the required scopes\n- Check firewall/network settings\n\n**Mirror Creation Fails**\n- Verify both source and target projects exist\n- Ensure the access token has write permissions\n- Check that the projects are not already being mirrored\n\n**Import Fails or Has Errors**\n- **Check the detailed error messages** - The import results show exactly which mirrors failed and why (e.g., `[3/10] group/bad → mirror/bad: Project not found`)\n- **Validate JSON format** - Must have `mirrors` array with `source_project_path` and `target_project_path` fields\n- **Ensure projects exist** - Both source and target projects must exist in their respective GitLab instances\n- **Check project paths** - Must use full paths like `namespace/project` or `group/subgroup/project` (not just project names)\n- **Verify pair selection** - Make sure you have the correct instance pair selected before importing\n- **Check GitLab tokens** - Instance tokens need `api` scope to look up projects and create mirrors\n\n### Logs\n\nView application logs:\n```bash\n# Docker\ndocker-compose logs -f\n\n# Local development\n# Logs will be printed to console\n```\n\nSet log level in `.env`:\n```bash\nLOG_LEVEL=DEBUG  # Options: DEBUG, INFO, WARNING, ERROR\n```\n\n## Development\n\n### Running Tests\n```bash\npip install -e \".[dev]\"\npytest\n```\n\n### Live GitLab End-to-End Tests (opt-in)\n\nThe project includes comprehensive E2E tests that provision temporary projects and configure mirrors against **real GitLab instances**. These tests create realistic project content (files, branches, tags) and verify that mirroring actually works.\n\n#### Test Categories\n\n| Marker | Description | Test Files |\n|--------|-------------|------------|\n| `live_gitlab` | All live GitLab tests | All `test_e2e_*.py` files |\n| `multi_project` | Multiple projects in one group | `test_e2e_multi_project.py` |\n| `dual_instance` | Cross-instance mirroring | `test_e2e_cross_instance.py` |\n\n#### Environment Variables\n\n**Required for all E2E tests:**\n```bash\nexport E2E_LIVE_GITLAB=1                          # opt-in guard (must be set)\nexport E2E_GITLAB_URL=\"https://gitlab.example.com\"\nexport E2E_GITLAB_TOKEN=\"glpat-...\"               # must be able to create/delete projects, groups, and mirrors\nexport E2E_GITLAB_GROUP_PATH=\"my-group/subgroup\"  # group where test resources will be created\n```\n\n**Additional variables for dual-instance tests:**\n```bash\nexport E2E_GITLAB_URL_2=\"https://gitlab2.example.com\"\nexport E2E_GITLAB_TOKEN_2=\"glpat-...\"\nexport E2E_GITLAB_GROUP_PATH_2=\"target-group\"\n```\n\n**Optional tuning:**\n```bash\nexport E2E_GITLAB_HTTP_USERNAME=\"oauth2\"          # username for HTTPS clone auth (default: oauth2)\nexport E2E_GITLAB_MIRROR_TIMEOUT_S=\"120\"          # timeout for mirror sync (default: 120)\nexport E2E_KEEP_RESOURCES=1                       # skip cleanup - keep projects/groups for manual inspection\n```\n\n#### Running E2E Tests\n\n```bash\n# Run all single-instance E2E tests\npytest -m \"live_gitlab and not dual_instance\" -v\n\n# Run only multi-project tests\npytest -m multi_project -v\n\n# Run cross-instance tests (requires two GitLab instances)\npytest -m dual_instance -v\n\n# Run all E2E tests (single + dual instance)\npytest -m live_gitlab -v\n```\n\n#### What the Tests Do\n\n**Multi-Project Tests** (`test_e2e_multi_project.py`):\n- Creates 3 projects with different content (Python, JavaScript, Go templates)\n- Each project has multiple files, branches (main, develop, feature/*), and tags\n- Tests both push and pull mirroring\n- Verifies content (commits, branches, tags, files) syncs correctly\n\n**Cross-Instance Tests** (`test_e2e_cross_instance.py`):\n- Creates source projects on instance 1\n- Creates empty target projects on instance 2\n- Sets up push/pull mirrors between instances\n- Verifies content propagates across GitLab instances\n\n#### Cleanup\n\nAll tests automatically clean up created resources (projects, groups, mirrors) in a `finally` block, even if tests fail. Resources are deleted in reverse creation order to respect dependencies.\n\n**Keep resources for manual inspection:**\n```bash\nE2E_KEEP_RESOURCES=1 pytest -m multi_project -v\n```\n\nWhen `E2E_KEEP_RESOURCES=1` is set, the test will:\n- Skip deleting GitLab projects and groups\n- Print a summary of all created resources with their IDs\n- Leave mirrors configured on the projects so you can inspect them in GitLab\n\nThis is useful for debugging or manually exploring the test setup. Remember to delete the resources manually when done (delete projects first, then groups).\n\n### Run Live GitLab E2E via GitHub Actions (manual)\n\nThis repo includes a manual workflow: `.github/workflows/e2e-live-gitlab.yml`.\n\n**Repository Secrets (add in Settings → Secrets):**\n- `E2E_GITLAB_TOKEN` (required)\n- `E2E_GITLAB_URL` (recommended)\n- `E2E_GITLAB_GROUP_PATH` (recommended)\n- `E2E_GITLAB_TOKEN_2` (for dual-instance tests)\n- `E2E_GITLAB_URL_2` (for dual-instance tests)\n- `E2E_GITLAB_GROUP_PATH_2` (for dual-instance tests)\n\n**Trigger the workflow:**\n1. Go to Actions → **Live GitLab E2E (manual)** → Run workflow\n2. Select test scope: `single`, `dual`, `multi-project`, or `all`\n3. Optionally override URLs and group paths in the dispatch inputs\n\n### Code Style\nThe project follows standard Python conventions:\n- Use Black for code formatting\n- Follow PEP 8 guidelines\n- Write descriptive commit messages\n\n### Contributing\nContributions are welcome! Please:\n1. Fork the repository\n2. Create a feature branch\n3. Make your changes\n4. Submit a pull request\n\n## Roadmap\n\n- [x] Mirror status monitoring dashboard (Live dashboard with charts and real-time updates ✨)\n- [x] Dark mode support (Beautiful theme with smooth transitions ✨)\n- [x] Enhanced topology visualization (Animated particles, zoom controls, hover highlighting ✨)\n- [ ] Support for scheduled mirror synchronization\n- [ ] Email notifications for mirror failures\n- [ ] Support for SSH-based mirroring\n- [ ] Multi-user support with role-based access\n- [x] Advanced filtering and search\n- [x] Mirror health checks and diagnostics\n- [x] PostgreSQL database support\n\n## Related Projects\n\n- [issue-bridge](https://github.com/MrZoller/issue-bridge) - Synchronize GitLab issues across instances\n\n## License\n\nMIT License - see LICENSE file for details\n\n## Support\n\nFor issues, questions, or contributions:\n- GitHub Issues: [MrZoller/mirror-maestro issues](https://github.com/MrZoller/mirror-maestro/issues)\n- Documentation: This README and inline code documentation\n\n## Acknowledgments\n\n- Built with [FastAPI](https://fastapi.tiangolo.com/)\n- GitLab integration via [python-gitlab](https://python-gitlab.readthedocs.io/)\n- Inspired by [issue-bridge](https://github.com/MrZoller/issue-bridge)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrzoller%2Fmirror-maestro","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmrzoller%2Fmirror-maestro","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrzoller%2Fmirror-maestro/lists"}