diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml
deleted file mode 100644
index cee1627..0000000
--- a/.forgejo/workflows/deploy.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-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
deleted file mode 100644
index f13528a..0000000
--- a/.forgejo/workflows/docker-publish.yml
+++ /dev/null
@@ -1,63 +0,0 @@
-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
new file mode 100644
index 0000000..1230149
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "daily"
diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml
new file mode 100644
index 0000000..f4851b4
--- /dev/null
+++ b/.github/workflows/deploy.yaml
@@ -0,0 +1,36 @@
+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
new file mode 100644
index 0000000..351e43e
--- /dev/null
+++ b/.github/workflows/docker-publish.yml
@@ -0,0 +1,86 @@
+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 909cef3..1bb4f69 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,23 @@
-# Fizika
+# 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
diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md
new file mode 100644
index 0000000..124ed9c
--- /dev/null
+++ b/backend/CLAUDE.md
@@ -0,0 +1,134 @@
+# 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 4df2b9e..e267374 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -17,6 +17,7 @@ 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
new file mode 100644
index 0000000..a72505e
--- /dev/null
+++ b/backend/docker-compose.yml
@@ -0,0 +1,70 @@
+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 80b2086..4739746 100644
--- a/backend/public/admin.js
+++ b/backend/public/admin.js
@@ -1,11 +1,5 @@
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 8dd795a..068322b 100644
--- a/backend/public/index.html
+++ b/backend/public/index.html
@@ -10,6 +10,13 @@
data-api="https://stats.schmelczer.dev/status"
src="https://stats.schmelczer.dev/js/script.file-downloads.hash.outbound-links.js"
>
+