# yaml-language-server: $schema=https://schema.zeabur.app/template.json
apiVersion: zeabur.com/v1
kind: Template
metadata:
    name: emjudge
spec:
    description: A frontend assignment submission and auto-grading platform. External worker required for grading.
    coverImage: https://raw.githubusercontent.com/elvisdragonmao/emjudge/main/apps/web/public/og.png
    icon: https://raw.githubusercontent.com/elvisdragonmao/emjudge/main/apps/web/public/ej.svg
    variables:
        - key: PUBLIC_DOMAIN
          type: DOMAIN
          name: Domain
          description: The public domain for your emjudge platform
        - key: JWT_SECRET
          type: STRING
          name: JWT Secret
          description: Secret key for JWT token signing
        - key: DEFAULT_ADMIN_PASSWORD
          type: STRING
          name: Admin Password
          description: Initial password for the default admin account
    tags:
        - Tool
        - Education
    readme: |-
        # emjudge

        Frontend assignment submission and auto-grading platform. Students upload HTML/CSS/JS or React assignments, and the system runs Playwright tests in isolated Docker environments to produce scores, test results, screenshots, and logs.

        ![emjudge demo](https://raw.githubusercontent.com/elvisdragonmao/emjudge/main/apps/web/public/screenshots.webp)

        ## Features

        - **Multiple assignment types**: Support for HTML/CSS/JS and React projects
        - **Automated grading**: Playwright-based testing with detailed results
        - **Class management**: Organize students into classes with teachers
        - **Score tracking**: Visual score history with charts
        - **File management**: MinIO-based file storage for submissions and artifacts

        ## Services

        | Service    | Description |
        |------------|-------------|
        | PostgreSQL | Database for application data and judge jobs |
        | MinIO      | Object storage for submissions and artifacts |
        | API        | Fastify REST API backend built from this repository with Docker |
        | Web        | React SPA built from this repository with Docker and served by Nginx |
        | Caddy      | Public entrypoint that routes `/api/*` to API, `/img/*` to MinIO, and everything else to Web |

        ## Default admin account

        After deployment, log in with:

        - Username: `admin`
        - Password: the value you set in `DEFAULT_ADMIN_PASSWORD`

        ## Important notes

        - This template deploys the web app, API, database, and object storage
        - The Judge Worker is **not** deployed on Zeabur by this template because it needs local Docker access to run untrusted submissions safely
        - API startup automatically runs database migration and idempotent admin seeding before booting the server

        ## Worker setup (external machine)

        The Judge Worker polls the database for jobs. It needs outbound access to PostgreSQL and MinIO, but no inbound ports.

        ### 1. Get the PostgreSQL external connection string

        In the Zeabur Dashboard, open the **PostgreSQL** service and copy **Connection String (external, for Worker)** from **Instructions**.

        ### 2. Expose MinIO for external access

        In the Zeabur Dashboard, open the **MinIO** service, go to **Domains**, and bind a domain such as `minio.yourdomain.com` so MinIO is reachable over HTTPS on port `443`.

        ### 3. Run the worker

        ```bash
        git clone <your-repo>
        cd frontend-judge
        pnpm install

        docker build -t judge-runner:latest docker/judge-runner/

        pnpm --filter @judge/shared build
        pnpm --filter @judge/worker build

        export DATABASE_URL='postgresql://root:<password>@hkg1.clusters.zeabur.com:<port>/zeabur'
        export MINIO_ENDPOINT='minio.yourdomain.com'
        export MINIO_PORT='443'
        export MINIO_ACCESS_KEY='minio'
        export MINIO_SECRET_KEY='<your-minio-secret>'
        export MINIO_USE_SSL='true'
        node apps/worker/dist/index.js
        ```

        The worker will connect to the remote database, poll pending jobs, run them in local Docker containers, and upload artifacts back to MinIO.
    services:
        - name: PostgreSQL
          icon: https://cdn.zeabur.com/marketplace/postgresql.svg
          template: PREBUILT
          spec:
            id: postgresql
            source:
                image: postgres:18
                command:
                    - docker-entrypoint.sh
                    - -c
                    - config_file=/etc/postgresql/postgresql.conf
            ports:
                - id: database
                  port: 5432
                  type: TCP
            volumes:
                - id: data
                  dir: /var/lib/postgresql/18/docker
            instructions:
                - title: Connection String
                  content: postgresql://${POSTGRES_USERNAME}:${POSTGRES_PASSWORD}@${PORT_FORWARDED_HOSTNAME}:${DATABASE_PORT_FORWARDED_PORT}/${POSTGRES_DATABASE}
                - title: PostgreSQL Connect Command
                  content: psql "postgresql://${POSTGRES_USERNAME}:${POSTGRES_PASSWORD}@${PORT_FORWARDED_HOSTNAME}:${DATABASE_PORT_FORWARDED_PORT}/${POSTGRES_DATABASE}"
                - title: PostgreSQL username
                  content: ${POSTGRES_USERNAME}
                - title: PostgreSQL password
                  content: ${POSTGRES_PASSWORD}
                - title: PostgreSQL database
                  content: ${POSTGRES_DATABASE}
                - title: PostgreSQL host
                  content: ${PORT_FORWARDED_HOSTNAME}
                - title: PostgreSQL port
                  content: ${DATABASE_PORT_FORWARDED_PORT}
            env:
                PGDATA:
                    default: /var/lib/postgresql/18/docker/pgdata
                POSTGRES_CONNECTION_STRING:
                    default: postgresql://${POSTGRES_USERNAME}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DATABASE}
                    expose: true
                POSTGRES_DATABASE:
                    default: ${POSTGRES_DB}
                    expose: true
                POSTGRES_DB:
                    default: zeabur
                POSTGRES_HOST:
                    default: ${CONTAINER_HOSTNAME}
                    expose: true
                POSTGRES_PASSWORD:
                    default: ${PASSWORD}
                    expose: true
                POSTGRES_PORT:
                    default: ${DATABASE_PORT}
                    expose: true
                POSTGRES_URI:
                    default: ${POSTGRES_CONNECTION_STRING}
                    expose: true
                POSTGRES_USER:
                    default: root
                POSTGRES_USERNAME:
                    default: ${POSTGRES_USER}
                    expose: true
            configs:
                - path: /etc/postgresql/postgresql.conf
                  template: |
                    listen_addresses = '*'
                    max_connections = 256
                    shared_buffers = 256MB
                    dynamic_shared_memory_type = posix
                    max_wal_size = 1GB
                    min_wal_size = 80MB
                    log_timezone = 'UTC'
                    datestyle = 'iso, mdy'
                    timezone = 'UTC'
                    lc_messages = 'en_US.UTF-8'
                    lc_monetary = 'en_US.UTF-8'
                    lc_numeric = 'en_US.UTF-8'
                    lc_time = 'en_US.UTF-8'
                    default_text_search_config = 'pg_catalog.english'
                  permission: null
                  envsubst: null
        - name: MinIO
          icon: https://cdn.zeabur.com/marketplace/minio.svg
          template: PREBUILT
          spec:
            id: minio
            source:
                image: quay.io/minio/minio:latest
                command:
                    - /bin/sh
                args:
                    - -c
                    - |
                      minio server /data --console-address :9090 &
                      MINIO_PID=$!
                      while ! curl -s http://localhost:9000/minio/health/live; do
                        echo 'Waiting for MinIO to start...'
                        sleep 1
                      done
                      sleep 5
                      mc alias set myminio http://localhost:9000 $MINIO_USERNAME $MINIO_PASSWORD
                      mc mb myminio/submissions || true
                      mc mb myminio/artifacts || true
                      wait $MINIO_PID
            ports:
                - id: web
                  port: 9000
                  type: HTTP
                - id: console
                  port: 9090
                  type: HTTP
            volumes:
                - id: data
                  dir: /data
            instructions:
                - title: Access Key (Username)
                  content: ${MINIO_USERNAME}
                - title: Secret Key (Password)
                  content: ${MINIO_PASSWORD}
            env:
                MINIO_BROWSER_REDIRECT:
                    default: "false"
                MINIO_HOST:
                    default: ${CONTAINER_HOSTNAME}
                    expose: true
                MINIO_PASSWORD:
                    default: ${MINIO_ROOT_PASSWORD}
                    expose: true
                MINIO_PORT:
                    default: "9000"
                    expose: true
                MINIO_ROOT_PASSWORD:
                    default: ${PASSWORD}
                MINIO_ROOT_USER:
                    default: minio
                MINIO_USERNAME:
                    default: ${MINIO_ROOT_USER}
                    expose: true
        - name: API
          icon: https://service-icons.zeabur.com/git/nodejs/default.svg
          dependencies:
            - PostgreSQL
            - MinIO
          template: GIT
          spec:
            id: api
            source:
                source: GITHUB
                repo: 1183262376
                branch: main
                watchPaths:
                    - /apps/api
                    - /packages/shared
                    - /packages/config
                    - /package.json
                    - /pnpm-lock.yaml
                    - /pnpm-workspace.yaml
            ports:
                - id: web
                  port: 8080
                  type: HTTP
            env:
                API_HOST:
                    default: ${CONTAINER_HOSTNAME}
                    expose: true
                API_PORT:
                    default: "8080"
                    expose: true
                CORS_ORIGIN:
                    default: https://${PUBLIC_DOMAIN}
                DATABASE_URL:
                    default: ${POSTGRES_CONNECTION_STRING}
                DEFAULT_ADMIN_PASSWORD:
                    default: ${DEFAULT_ADMIN_PASSWORD}
                HOST:
                    default: 0.0.0.0
                JWT_EXPIRES_IN:
                    default: 7d
                JWT_SECRET:
                    default: ${JWT_SECRET}
                MINIO_ACCESS_KEY:
                    default: ${MINIO_USERNAME}
                MINIO_ENDPOINT:
                    default: ${MINIO_HOST}
                MINIO_PORT:
                    default: ${MINIO_PORT}
                MINIO_PUBLIC_BASE_URL:
                    default: /img
                MINIO_SECRET_KEY:
                    default: ${MINIO_PASSWORD}
                MINIO_USE_SSL:
                    default: "false"
                PORT:
                    default: "8080"
                ZBPACK_APP_DIR:
                    default: apps/api
                ZBPACK_BUILD_COMMAND:
                    default: cd ../.. && pnpm --filter @judge/shared build && pnpm --filter @judge/api build && pnpm --filter @judge/api db:migrate && pnpm --filter @judge/api db:seed
                ZBPACK_START_COMMAND:
                    default: node dist/index.js
            healthCheck:
                type: HTTP
                port: web
                http:
                    path: /api/health
        - name: Web
          icon: https://service-icons.zeabur.com/git/nodejs/default.svg
          dependencies:
            - API
          template: GIT
          spec:
            id: web
            source:
                source: GITHUB
                repo: 1183262376
                branch: main
                watchPaths:
                    - /apps/web
                    - /packages/shared
                    - /packages/config
                    - /package.json
                    - /pnpm-lock.yaml
                    - /pnpm-workspace.yaml
            ports:
                - id: web
                  port: 8080
                  type: HTTP
            env:
                PORT:
                    default: "8080"
                WEB_HOST:
                    default: ${CONTAINER_HOSTNAME}
                    expose: true
                WEB_PORT:
                    default: "8080"
                    expose: true
                ZBPACK_APP_DIR:
                    default: apps/web
                ZBPACK_BUILD_COMMAND:
                    default: cd ../.. && pnpm --filter @judge/shared build && pnpm --filter @judge/web build
        - name: Caddy
          icon: https://cdn.zeabur.com/caddy.png
          dependencies:
            - Web
            - API
            - MinIO
          template: PREBUILT
          spec:
            id: caddy
            source:
                image: caddy:2-alpine
            ports:
                - id: web
                  port: 80
                  type: HTTP
            configs:
                - path: /etc/caddy/Caddyfile
                  template: |
                    {
                      servers {
                        trusted_proxies static private_ranges
                        trusted_proxies_strict
                        client_ip_headers X-Forwarded-For X-Real-IP
                      }
                    }

                    :80 {
                      log
                      encode

                      handle /api/* {
                        reverse_proxy ${API_HOST}:${API_PORT}
                      }

                      handle /img/* {
                        uri strip_prefix /img
                        reverse_proxy ${MINIO_HOST}:${MINIO_PORT} {
                          header_up Host {upstream_hostport}
                        }
                      }

                      handle {
                        reverse_proxy ${WEB_HOST}:${WEB_PORT}
                      }
                    }
                  permission: null
                  envsubst: true
          domainKey: PUBLIC_DOMAIN
localization:
    zh-TW:
        description: 前端作業繳交與自動評測平台。評測 Worker 需在外部機器執行。
        variables:
            - key: PUBLIC_DOMAIN
              type: DOMAIN
              name: 網域
              description: 你想將 emjudge 綁定到哪個網域？
            - key: JWT_SECRET
              type: STRING
              name: JWT 密鑰
              description: JWT 簽名用的密鑰（請使用隨機字串）
            - key: DEFAULT_ADMIN_PASSWORD
              type: STRING
              name: 管理員密碼
              description: 預設管理員帳號的密碼
        readme: |-
            # emjudge

            前端作業繳交與自動評測平台。學生上傳 HTML/CSS/JS 或 React 作業，系統在隔離的 Docker 環境中用 Playwright 執行測試，產出分數、測試結果、截圖與 log。

            ![emjudge demo](https://raw.githubusercontent.com/elvisdragonmao/emjudge/main/apps/web/public/screenshots.webp)

            ## 功能特色

            - **多種作業類型**：支援 HTML/CSS/JS 和 React 專案
            - **自動評測**：基於 Playwright 的測試，提供詳細結果
            - **班級管理**：將學生組織成班級，由老師管理
            - **成績追蹤**：圖表化的成績歷史記錄
            - **檔案管理**：基於 MinIO 的檔案儲存系統

            ## 服務架構

            | 服務       | 說明 |
            |-----------|------|
            | PostgreSQL | 儲存應用資料與評測工作佇列 |
            | MinIO      | 儲存作業上傳檔案與評測產物 |
            | API        | 從此 repository 以 Docker 建置的 Fastify 後端 |
            | Web        | 從此 repository 以 Docker 建置並由 Nginx 提供的 React SPA |
            | Caddy      | 對外入口，將 `/api/*` 導到 API、`/img/*` 導到 MinIO，其餘導到 Web |

            ## 預設管理員帳號

            部署完成後可用以下帳號登入：

            - 帳號：`admin`
            - 密碼：你在 `DEFAULT_ADMIN_PASSWORD` 設定的值

            ## 重要說明

            - 這個模板會部署 Web、API、資料庫與物件儲存
            - Judge Worker **不會**由這個模板部署到 Zeabur，因為它需要本機 Docker 權限來安全執行使用者提交內容
            - API 啟動時會先自動執行資料庫 migration 與可重複執行的管理員 seed，再啟動伺服器

            ## Worker 設定（外部機器）

            Judge Worker 會輪詢資料庫取得工作，只需要能對外連到 PostgreSQL 與 MinIO，不需要開放任何入站 port。

            ### 1. 取得 PostgreSQL 外部連線字串

            在 Zeabur Dashboard 開啟 **PostgreSQL** 服務，到 **Instructions** 複製 **Connection String (external, for Worker)**。

            ### 2. 開放 MinIO 外部連線

            在 Zeabur Dashboard 開啟 **MinIO** 服務，進入 **Domains**，綁定一個 domain，例如 `minio.yourdomain.com`，讓它能透過 HTTPS `443` 對外提供存取。

            ### 3. 啟動 Worker

            ```bash
            git clone <your-repo>
            cd frontend-judge
            pnpm install

            docker build -t judge-runner:latest docker/judge-runner/

            pnpm --filter @judge/shared build
            pnpm --filter @judge/worker build

            export DATABASE_URL='postgresql://root:<password>@hkg1.clusters.zeabur.com:<port>/zeabur'
            export MINIO_ENDPOINT='minio.yourdomain.com'
            export MINIO_PORT='443'
            export MINIO_ACCESS_KEY='minio'
            export MINIO_SECRET_KEY='<your-minio-secret>'
            export MINIO_USE_SSL='true'
            node apps/worker/dist/index.js
            ```

            Worker 啟動後會連到遠端資料庫，持續輪詢待處理 job，在本機 Docker 執行評測，再把結果與 artifacts 上傳回 MinIO。
