Small fixes

This commit is contained in:
Andras Schmelczer 2025-08-31 13:51:59 +01:00
parent bb03d4bc14
commit 5e74ab8322
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
6 changed files with 201 additions and 85 deletions

134
backend/CLAUDE.md Normal file
View file

@ -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

View file

@ -1,36 +1,27 @@
const API_BASE = window.location.origin;
// Initialize
document.addEventListener("DOMContentLoaded", function () {
loadQuestions();
loadImages();
// Set up event listeners
setupEventListeners();
});
function setupEventListeners() {
// Tab switching
document.querySelectorAll('.tab[data-tab]').forEach(tab => {
tab.addEventListener('click', (e) => {
switchTab(e.target.dataset.tab);
});
});
// Add question button
document.getElementById('addQuestionBtn').addEventListener('click', showAddQuestionModal);
// Image upload
document.getElementById('imageUpload').addEventListener('change', uploadImage);
// Modal close buttons
document.getElementById('closeModalBtn').addEventListener('click', closeModal);
document.getElementById('cancelBtn').addEventListener('click', closeModal);
// Question form submit
document.getElementById('questionForm').addEventListener('submit', saveQuestion);
// Close modal when clicking outside
document.getElementById('questionModal').addEventListener('click', (e) => {
if (e.target.id === 'questionModal') {
closeModal();
@ -38,7 +29,6 @@ function setupEventListeners() {
});
}
// Tab switching
function switchTab(tabName) {
document
.querySelectorAll(".tab-content")
@ -53,7 +43,6 @@ function switchTab(tabName) {
if (tabName === "images") loadImages();
}
// Questions management
async function loadQuestions() {
try {
const response = await fetch(`${API_BASE}/api/admin/questions`);
@ -65,7 +54,7 @@ async function loadQuestions() {
}
} catch (error) {
document.getElementById("questionsList").innerHTML =
'<div class="alert alert-danger">Hiba a kérdések betöltésekor</div>';
`<div class="alert alert-danger">Hiba a kérdések betöltésekor ${error.message}</div>`;
}
}
@ -77,14 +66,12 @@ function displayQuestions(questions) {
<div class="question-item">
<h4>ID: ${q.id} - ${q.source}</h4>
<p><strong>Kérdés:</strong> ${q.description.substring(
0,
100
)}...</p>
<p><strong>Típus:</strong> ${
q.type
} | <strong>Helyes válasz:</strong> ${
["A", "B", "C", "D"][q.correct - 1]
}</p>
0,
100
)}...</p>
<p><strong>Típus:</strong> ${q.type
} | <strong>Helyes válasz:</strong> ${["A", "B", "C", "D"][q.correct - 1]
}</p>
<div class="question-actions">
<button data-edit-id="${q.id}">Szerkesztés</button>
<button class="danger" data-delete-id="${q.id}">Törlés</button>
@ -93,14 +80,13 @@ function displayQuestions(questions) {
`
)
.join("");
// Add event listeners for edit and delete buttons
container.querySelectorAll('[data-edit-id]').forEach(btn => {
btn.addEventListener('click', (e) => {
editQuestion(parseInt(e.target.dataset.editId));
});
});
container.querySelectorAll('[data-delete-id]').forEach(btn => {
btn.addEventListener('click', (e) => {
deleteQuestion(parseInt(e.target.dataset.deleteId));
@ -213,7 +199,6 @@ async function deleteQuestion(id) {
}
}
// Images management
async function loadImages() {
try {
const response = await fetch(`${API_BASE}/api/images`);
@ -242,8 +227,7 @@ function displayImages(images) {
`
)
.join("");
// Add event listeners for delete buttons
container.querySelectorAll('[data-delete-image]').forEach(btn => {
btn.addEventListener('click', (e) => {
deleteImage(e.target.dataset.deleteImage);
@ -279,7 +263,7 @@ async function uploadImage() {
);
}
} catch (error) {
showAlert("imagesAlert", "Kapcsolat hiba", "danger");
showAlert("imagesAlert", `Kapcsolat hiba: ${error.message}`, "danger");
}
}
@ -301,11 +285,10 @@ async function deleteImage(filename) {
throw new Error("Delete failed");
}
} catch (error) {
showAlert("imagesAlert", "Hiba a törlés során", "danger");
showAlert("imagesAlert", `Hiba a törlés során: ${error.message}`, "danger");
}
}
// Utility functions
function closeModal() {
document.getElementById("questionModal").style.display = "none";
}
@ -319,6 +302,4 @@ function showAlert(elementId, message, type) {
setTimeout(() => {
alertDiv.style.display = "none";
}, 5000);
}
// This is now handled in setupEventListeners()
}

View file

@ -338,9 +338,24 @@
<div class="form-group">
<label for="questionType">Típus:</label>
<select id="questionType" required>
<option value="mec">Mechanika</option>
<option value="mk">Kinematika</option>
<option value="md">Dinamika</option>
<option value="me">Mechanika</option>
<option value="mf">Folyadékok</option>
<option value="me">Munka és energia</option>
<option value="mf">Folyadékok és gázok mechanikája</option>
<option value="mr">Rezgések és hullámok</option>
<option value="h">Hőtan</option>
<option value="ele">Elektromosság</option>
<option value="es">Elektrosztatika</option>
<option value="ee">Egyenáram</option>
<option value="ev">Váltakozó áram</option>
<option value="m">Mágnesesség</option>
<option value="o">Fénytan</option>
<option value="atm">Atomfizika</option>
<option value="ah">Atomhéj</option>
<option value="am">Atommag</option>
<option value="cs">Égi mechanika, csillagászat</option>
<option value="v">Vegyes</option>
</select>
</div>

View file

@ -19,8 +19,8 @@ app.use(express.json({ limit: '100mb' }));
app.use(express.static('public'));
// File paths
const DATA_PATH = process.env.DATA_PATH || path.join(__dirname, '../fizika.json');
const PICS_PATH = process.env.PICS_PATH || path.join(__dirname, '../pics');
const DATA_PATH = process.env.DATA_PATH || path.join(__dirname, '../frontend/fizika.json');
const PICS_PATH = process.env.PICS_PATH || path.join(__dirname, '../frontend/pics');
// Multer configuration for image uploads
const storage = multer.diskStorage({
@ -54,11 +54,7 @@ const writeData = async (data) => {
await fs.writeFile(DATA_PATH, JSON.stringify(data, null, 2));
};
const validateQuestion = (q) => {
return q.description && q.a && q.b && q.c && q.d &&
q.correct >= 1 && q.correct <= 4 &&
['md', 'me', 'mf'].includes(q.type) && q.source;
};
// Public routes
app.get('/api/fizika', async (req, res) => {
@ -94,10 +90,6 @@ app.get('/api/admin/questions', async (req, res) => {
app.post('/api/admin/questions', async (req, res) => {
try {
if (!validateQuestion(req.body)) {
return res.status(400).json({ error: 'Invalid question data' });
}
const data = await readData();
const maxId = Math.max(...data.map(q => q.id), 0);
@ -113,10 +105,6 @@ app.post('/api/admin/questions', async (req, res) => {
app.put('/api/admin/questions/:id', async (req, res) => {
try {
if (!validateQuestion(req.body)) {
return res.status(400).json({ error: 'Invalid question data' });
}
const data = await readData();
const index = data.findIndex(q => q.id === parseInt(req.params.id));