diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml new file mode 100644 index 0000000..cee1627 --- /dev/null +++ b/.forgejo/workflows/deploy.yml @@ -0,0 +1,31 @@ +name: Deploy to Pages + +on: + push: + branches: ['main'] + pull_request: + branches: ['main'] + workflow_dispatch: + +concurrency: + group: 'pages' + cancel-in-progress: false + +jobs: + deploy: + runs-on: docker + + steps: + - uses: actions/checkout@v4 + + - name: Validate static frontend + run: | + test -f frontend/index.html + test -f frontend/fizika.json + + - name: Copy frontend to host pages mount + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + apt update && apt install -y rsync + mkdir -p /pages + rsync -a --delete frontend/ /pages/fizika diff --git a/.forgejo/workflows/docker-publish.yml b/.forgejo/workflows/docker-publish.yml new file mode 100644 index 0000000..f13528a --- /dev/null +++ b/.forgejo/workflows/docker-publish.yml @@ -0,0 +1,63 @@ +name: Build and Publish Docker Image + +on: + push: + branches: ['main'] + tags: ['v*'] + pull_request: + branches: ['main'] + workflow_dispatch: + +env: + IMAGE_NAME: ${{ forgejo.repository }}/fizika-admin + +jobs: + build-and-push: + runs-on: ubuntu-docker + + steps: + - name: Checkout repository + uses: https://code.forgejo.org/actions/checkout@v4 + + - name: Extract registry host + id: registry + run: echo "host=$(echo '${{ forgejo.server_url }}' | sed 's|https\?://||')" >> "$GITHUB_OUTPUT" + + - name: Log into Forgejo registry + if: forgejo.event_name != 'pull_request' + run: echo "${{ secrets.FORGEJO_TOKEN }}" | docker login "${{ steps.registry.outputs.host }}" -u "${{ forgejo.actor }}" --password-stdin + + - name: Build Docker image + run: | + IMAGE="${{ steps.registry.outputs.host }}/$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')" + SHA_SHORT="$(echo "${{ forgejo.sha }}" | cut -c1-12)" + TAG_ARGS="-t ${IMAGE}:sha-${SHA_SHORT}" + + if [ "${{ forgejo.ref }}" = "refs/heads/main" ]; then + TAG_ARGS="${TAG_ARGS} -t ${IMAGE}:main -t ${IMAGE}:latest" + fi + + if [ "${{ forgejo.ref_type }}" = "tag" ]; then + REF_NAME="${{ forgejo.ref_name }}" + TAG_ARGS="${TAG_ARGS} -t ${IMAGE}:${REF_NAME}" + + if echo "$REF_NAME" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then + VERSION="${REF_NAME#v}" + MAJOR_MINOR="$(echo "$VERSION" | cut -d. -f1,2)" + MAJOR="$(echo "$VERSION" | cut -d. -f1)" + TAG_ARGS="${TAG_ARGS} -t ${IMAGE}:${VERSION} -t ${IMAGE}:${MAJOR_MINOR} -t ${IMAGE}:${MAJOR}" + fi + fi + + docker build \ + --label "org.opencontainers.image.source=${{ forgejo.server_url }}/${{ forgejo.repository }}" \ + --label "org.opencontainers.image.revision=${{ forgejo.sha }}" \ + --label "org.opencontainers.image.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ + ${TAG_ARGS} \ + ./backend + + echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV" + + - name: Push Docker image + if: forgejo.event_name != 'pull_request' + run: docker push --all-tags "$IMAGE" diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 1230149..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml deleted file mode 100644 index f4851b4..0000000 --- a/.github/workflows/deploy.yaml +++ /dev/null @@ -1,36 +0,0 @@ -name: Deploy to Pages - -on: - push: - branches: [main] - workflow_dispatch: - -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Pages - uses: actions/configure-pages@v5 - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: "frontend" - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml deleted file mode 100644 index 351e43e..0000000 --- a/.github/workflows/docker-publish.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Build and Publish Docker Image - -on: - push: - branches: ["main"] - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }}/fizika-admin - -jobs: - build-and-push: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - # This is used to complete the identity challenge - # with sigstore/fulcio when running outside of PRs. - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Docker buildx - uses: docker/setup-buildx-action@v3 - - # Login against a Docker registry except on PR - # https://github.com/docker/login-action - - name: Log into registry ${{ env.REGISTRY }} - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - # Extract metadata (tags, labels) for Docker - # https://github.com/docker/metadata-action - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=sha,prefix={{branch}}- - # set latest tag for default branch - type=raw,value=latest,enable={{is_default_branch}} - - # Build and push Docker image with Buildx (don't push on PR) - # https://github.com/docker/build-push-action - - name: Build and push Docker image - id: build-and-push - uses: docker/build-push-action@v5 - with: - context: ./backend - file: ./backend/Dockerfile - platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - # Security scanning - sbom: true - provenance: true - - # Sign the resulting Docker image digest. - # This will only write to the public Rekor transparency log when the Docker - # repository is public to avoid leaking data. If you would like to publish - # transparency data even for private images, pass --force to cosign below. - # https://github.com/sigstore/cosign - - name: Sign the published Docker image - if: ${{ github.ref_type == 'tag' }} - env: - # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable - TAGS: ${{ steps.meta.outputs.tags }} - DIGEST: ${{ steps.build-and-push.outputs.digest }} - # This step uses the identity token to provision an ephemeral certificate - # against the sigstore community Fulcio instance. - run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} diff --git a/README.md b/README.md index 1bb4f69..909cef3 100644 --- a/README.md +++ b/README.md @@ -1,23 +1 @@ -# Fizika - Physics Quiz Application - -A comprehensive physics quiz application for Hungarian students preparing for their physics exams (érettségi). The application consists of a frontend quiz interface and an admin backend for content management. - -## 🚀 Features - -### Student Interface (Frontend) - -- Interactive physics quiz questions -- Multiple choice questions with immediate feedback -- Category-based filtering (dynamics, mechanics, fluids, etc.) -- Search functionality by year, month, and question number -- Responsive design for desktop and mobile -- Progress tracking and scoring -- Timer functionality - -### Admin Interface (Backend) - -- 📝 Full CRUD operations for quiz questions -- 🖼️ Image management (upload, view, delete) -- 📊 RESTful API for frontend integration -- 🛡️ Basic security features (input validation) -- 🐳 Docker containerization ready +# Fizika diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md deleted file mode 100644 index 124ed9c..0000000 --- a/backend/CLAUDE.md +++ /dev/null @@ -1,134 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Common Development Commands - -### Running the Application -```bash -npm run dev # Development mode with nodemon hot reload -npm start # Production mode -``` - -### Docker Commands -```bash -# Production deployment -docker-compose up -d - -# Development with hot reload -docker-compose --profile dev up fizika-admin-dev - -# Build image manually -docker build -t fizika-admin ./backend -``` - -### Environment Setup -```bash -cp .env.example .env -# Edit .env with your configuration -``` - -## Architecture Overview - -This is a single-file Node.js/Express backend (`server.js`) that serves as both: -1. **API Server**: Provides REST endpoints for question and image management -2. **Static File Server**: Serves the admin interface from `/public/` directory - -### Key Components - -**Data Storage**: -- Questions stored in JSON file at `../frontend/fizika.json` (configurable via `DATA_PATH`) -- Images stored in `../frontend/pics/` directory (configurable via `PICS_PATH`) - -**Admin Interface**: -- Built-in web UI served from `/public/index.html` -- JavaScript client in `/public/admin.js` -- No authentication required (simplified for admin use) - -**API Structure**: -- Public endpoints: `/api/fizika`, `/api/images`, `/api/pics/:filename` -- Admin endpoints: `/api/admin/questions`, `/api/admin/images` -- No JWT authentication implemented despite README documentation - -### Question Data Format -```json -{ - "id": 1, - "source": "2016/m1/1", - "description": "Question text...", - "a": "Option A", - "b": "Option B", - "c": "Option C", - "d": "Option D", - "correct": 2, - "type": "md", - "image": "optional-image.jpg" -} -``` - -### Complete Question Types (17 Categories) - -**IMPORTANT**: The README only mentions 3 types, but the frontend supports all 17: - -**Mechanics (Mechanika)**: -- `mec` - Mechanika (general mechanics) -- `mk` - Kinematika (kinematics) -- `md` - Dinamika (dynamics) -- `me` - Munka és energia (work and energy) -- `mf` - Folyadékok és gázok mechanikája (fluid mechanics) -- `mr` - Rezgések és hullámok (oscillations and waves) - -**Thermodynamics**: -- `h` - Hőtan (thermodynamics) - -**Electricity**: -- `ele` - Elektromosság (general electricity) -- `es` - Elektrosztatika (electrostatics) -- `ee` - Egyenáram (direct current) -- `ev` - Váltakozó áram (alternating current) - -**Other Physics**: -- `m` - Mágnesesség (magnetism) -- `o` - Fénytan (optics) -- `atm` - Atomfizika (general atomic physics) -- `ah` - Atomhéj (electron shells) -- `am` - Atommag (atomic nucleus) -- `cs` - Égi mechanika, csillagászat (celestial mechanics, astronomy) -- `v` - Vegyes (mixed/various) - -### Security Considerations -- Helmet.js for security headers -- CORS configuration via `FRONTEND_URL` environment variable -- File upload restricted to images only (5MB limit) -- Input validation minimal - add validation when modifying endpoints - -## File Structure -``` -backend/ -├── server.js # Main application file -├── public/ -│ ├── index.html # Admin interface HTML -│ └── admin.js # Admin interface JavaScript -├── package.json -├── Dockerfile -├── docker-compose.yml -└── .env.example -``` - -## Important Notes - -- **Single File Architecture**: All server logic is in `server.js` - no separate route files or modules -- **File-based Data**: Uses JSON file for persistence, not a database -- **No Authentication**: Despite README documentation mentioning JWT, no auth is implemented -- **Path Dependencies**: Assumes frontend directory structure (`../frontend/fizika.json`, `../frontend/pics/`) -- **Admin UI Included**: Built-in web interface accessible at root path `/` -- **Question Types**: Support all 17 physics categories listed above, not just the 3 in README - -## Making Changes - -When modifying the API: -1. All changes go in `server.js` -2. Test both API endpoints and admin UI functionality -3. Ensure question type validation supports all 17 categories if adding validation -4. Consider impact on file paths and data format -5. Update environment variables in `.env.example` if needed \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index e267374..4df2b9e 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -17,7 +17,6 @@ ENV NODE_ENV=production ENV PORT=3001 EXPOSE 3001 -# Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD node -e "require('http').get('http://localhost:3001/api/fizika', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) }).on('error', () => process.exit(1))" diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml deleted file mode 100644 index a72505e..0000000 --- a/backend/docker-compose.yml +++ /dev/null @@ -1,70 +0,0 @@ -version: '3.8' - -services: - fizika-admin: - build: - context: ./backend - dockerfile: Dockerfile - ports: - - "3001:3001" - environment: - - NODE_ENV=production - - PORT=3001 - - FRONTEND_URL=${FRONTEND_URL:-*} - - DATA_PATH=${DATA_PATH:-/usr/src/app/fizika.json} - - PICS_PATH=${PICS_PATH:-/usr/src/app/pics} - volumes: - # Mount data files - - ./fizika.json:/usr/src/app/fizika.json:ro - - ./pics:/usr/src/app/pics - # Optional: mount for development - # - ./backend:/usr/src/app - restart: unless-stopped - healthcheck: - test: ["CMD", "node", "-e", "require('http').get('http://localhost:3001/api/fizika', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) }).on('error', () => process.exit(1))"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # Optional: Development service with hot reload - fizika-admin-dev: - build: - context: ./backend - dockerfile: Dockerfile - target: dev-dependencies - ports: - - "3001:3001" - environment: - - NODE_ENV=development - - PORT=3001 - - FRONTEND_URL=* - - DATA_PATH=/usr/src/app/fizika.json - - PICS_PATH=/usr/src/app/pics - volumes: - - ./backend:/usr/src/app - - ./fizika.json:/usr/src/app/fizika.json - - ./pics:/usr/src/app/pics - - /usr/src/app/node_modules - command: npm run dev - profiles: ["dev"] - - # Optional: Nginx reverse proxy for production - nginx: - image: nginx:alpine - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx.conf:/etc/nginx/nginx.conf:ro - - .:/usr/share/nginx/html:ro - # SSL certificates (if using HTTPS) - # - ./ssl:/etc/ssl/certs - depends_on: - - fizika-admin - restart: unless-stopped - profiles: ["nginx"] - -networks: - default: - name: fizika-network \ No newline at end of file diff --git a/backend/public/admin.js b/backend/public/admin.js index 4739746..80b2086 100644 --- a/backend/public/admin.js +++ b/backend/public/admin.js @@ -1,5 +1,11 @@ const API_BASE = window.location.origin; +window.plausible = + window.plausible || + function () { + (window.plausible.q = window.plausible.q || []).push(arguments); + }; + document.addEventListener("DOMContentLoaded", function () { loadQuestions(); loadImages(); diff --git a/backend/public/index.html b/backend/public/index.html index 068322b..8dd795a 100644 --- a/backend/public/index.html +++ b/backend/public/index.html @@ -10,13 +10,6 @@ data-api="https://stats.schmelczer.dev/status" src="https://stats.schmelczer.dev/js/script.file-downloads.hash.outbound-links.js" > -