Compare commits

..

12 Commits

Author SHA1 Message Date
herel dd9bdf84f3 fix: update dependencies (1.4.1) 2026-07-01 16:09:24 +02:00
herel 80f4b4047c chore(deploy): 512Mi ram 2026-07-01 15:56:08 +02:00
herel cc64d126be fix(api): Add upper bound validation to num_exercises
The /generate and /generate-operations endpoints accepted num_exercises
without an upper bound, allowing DoS via excessively large values.
Add Field(le=100) to both ExerciseRequest and OperationExerciseRequest
Pydantic models to cap exercises at 100.

Fixes vuln-0001
2026-07-01 15:07:10 +02:00
herel e4db258bc5 fix: update version 2025-12-10 11:38:16 +01:00
herel 449adc60e0 feat: dark mode and python 3.14 2025-12-10 11:36:40 +01:00
herel d68dc8411f chore: speedup readyness 2025-10-15 17:25:08 +02:00
herel 60f776c56d chore: service internal traffic policy 2025-10-15 17:24:13 +02:00
herel 7228dc4ec6 chore: add scale-to-zero overlay 2025-10-15 17:11:30 +02:00
herel e4989dcc8d chore: bump production image tag to 1.2.0 2025-10-07 08:47:52 +02:00
herel 3fdbfcf20e feat: replace checkbox with radio buttons for exercise type selection
- Replace the checkbox "Générer uniquement des multiplications" with radio buttons for "Multiplications uniquement" and "Multiplications ET divisions"
- Update JavaScript code to handle radio button values instead of checkbox
- Maintain backward compatibility with old localStorage format
- Ensure all functionality remains intact
2025-10-07 08:46:38 +02:00
herel b17afbb5cd chore: bump production image tag to 1.1.0 2025-10-07 08:34:21 +02:00
herel 711a26c26c chore: bump production image tag to 1.0.10 2025-10-07 08:33:13 +02:00
10 changed files with 333 additions and 46 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
# Use Python 3.13 slim image as base # Use Python 3.13 slim image as base
FROM python:3.13-slim AS builder FROM python:3.14-slim AS builder
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app
@@ -17,7 +17,7 @@ RUN --mount=type=cache,id=pip,target=/root/.cache/pip \
--target=/app/site-packages \ --target=/app/site-packages \
-r requirements.txt -r requirements.txt
FROM python:3.13-slim FROM python:3.14-slim
COPY --from=builder /app/site-packages /app/site-packages COPY --from=builder /app/site-packages /app/site-packages
+4 -4
View File
@@ -8,7 +8,7 @@ from typing import List
from fastapi import FastAPI, Request, Form from fastapi import FastAPI, Request, Form
from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from pydantic import BaseModel from pydantic import BaseModel, Field
from fpdf import FPDF from fpdf import FPDF
import logging import logging
@@ -68,13 +68,13 @@ class MathExercisesPDF(FPDF):
class ExerciseRequest(BaseModel): class ExerciseRequest(BaseModel):
min_table: int min_table: int
max_table: int max_table: int
num_exercises: int = 15 num_exercises: int = Field(15, ge=1, le=100)
multiplication_only: bool = False multiplication_only: bool = False
max_first_operand: int = 12 # New parameter for the maximum value of the first operand max_first_operand: int = Field(12, ge=1, le=12) # New parameter for the maximum value of the first operand
class OperationExerciseRequest(BaseModel): class OperationExerciseRequest(BaseModel):
num_exercises: int = 20 num_exercises: int = Field(20, ge=1, le=100)
def generate_exercises( def generate_exercises(
+202 -9
View File
@@ -5,17 +5,42 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Générateur d'Exercices de Mathématiques</title> <title>Générateur d'Exercices de Mathématiques</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<style> <style>
:root {
--bs-body-bg: #ffffff;
--bs-body-color: #212529;
--bs-border-color: #dee2e6;
--bs-secondary-bg: #f8f9fa;
--bs-tertiary-bg: #ffffff;
}
[data-bs-theme="dark"] {
--bs-body-bg: #121212;
--bs-body-color: #e9ecef;
--bs-border-color: #495057;
--bs-secondary-bg: #212529;
--bs-tertiary-bg: #343a40;
}
body {
background-color: var(--bs-body-bg);
color: var(--bs-body-color);
transition: background-color 0.3s ease, color 0.3s ease;
}
.card { .card {
box-shadow: 0 4px 8px rgba(0,0,0,0.1); box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transition: 0.3s; transition: 0.3s;
margin-bottom: 20px; margin-bottom: 20px;
background-color: var(--bs-tertiary-bg);
border-color: var(--bs-border-color);
} }
.card:hover { .card:hover {
box-shadow: 0 8px 16px rgba(0,0,0,0.2); box-shadow: 0 8px 16px rgba(0,0,0,0.2);
} }
.exercise-result { .exercise-result {
background-color: #f8f9fa; background-color: var(--bs-secondary-bg);
border-radius: 5px; border-radius: 5px;
padding: 15px; padding: 15px;
margin-top: 20px; margin-top: 20px;
@@ -25,9 +50,81 @@
height: 3rem; height: 3rem;
margin-right: 0.75rem; margin-right: 0.75rem;
} }
.dark-mode-toggle {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
border-radius: 50%;
width: 50px;
height: 50px;
border: none;
background-color: var(--bs-tertiary-bg);
color: var(--bs-body-color);
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
.dark-mode-toggle:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
[data-bs-theme="dark"] .bg-light {
background-color: var(--bs-secondary-bg) !important;
}
[data-bs-theme="dark"] .text-muted {
color: var(--bs-body-color) !important;
opacity: 0.7;
}
[data-bs-theme="dark"] .table {
--bs-table-bg: var(--bs-tertiary-bg);
--bs-table-striped-bg: var(--bs-secondary-bg);
--bs-table-hover-bg: var(--bs-border-color);
}
[data-bs-theme="dark"] .alert {
border-color: var(--bs-border-color);
}
[data-bs-theme="dark"] .form-control,
[data-bs-theme="dark"] .form-select {
background-color: var(--bs-tertiary-bg);
border-color: var(--bs-border-color);
color: var(--bs-body-color);
}
[data-bs-theme="dark"] .form-control:focus,
[data-bs-theme="dark"] .form-select:focus {
background-color: var(--bs-tertiary-bg);
border-color: var(--bs-primary);
color: var(--bs-body-color);
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
[data-bs-theme="dark"] .form-check-input {
background-color: var(--bs-tertiary-bg);
border-color: var(--bs-border-color);
}
[data-bs-theme="dark"] .form-check-input:checked {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
}
.transition-all {
transition: all 0.3s ease;
}
</style> </style>
</head> </head>
<body> <body>
<!-- Dark Mode Toggle Button -->
<button class="dark-mode-toggle" id="darkModeToggle" title="Basculer le mode sombre" aria-label="Basculer le mode sombre">
<i class="bi bi-moon-fill" id="darkModeIcon"></i>
</button>
<div class="container mt-5 mb-5"> <div class="container mt-5 mb-5">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-lg-8"> <div class="col-lg-8">
@@ -61,9 +158,20 @@
<div class="form-text">Nombre total d'exercices à générer (entre 1 et 100)</div> <div class="form-text">Nombre total d'exercices à générer (entre 1 et 100)</div>
</div> </div>
<div class="mb-3 form-check"> <div class="mb-3">
<input type="checkbox" class="form-check-input" id="multiplicationOnly"> <label class="form-label">Types d'exercices</label>
<label class="form-check-label" for="multiplicationOnly">Générer uniquement des multiplications</label> <div class="form-check">
<input class="form-check-input" type="radio" name="exerciseType" id="multiplicationOnly" value="multiplication" checked>
<label class="form-check-label" for="multiplicationOnly">
Multiplications uniquement
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="exerciseType" id="multiplicationAndDivision" value="both">
<label class="form-check-label" for="multiplicationAndDivision">
Multiplications ET divisions
</label>
</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -240,7 +348,7 @@
</div> </div>
</div> </div>
<footer class="bg-light text-center text-muted py-4 mt-5"> <footer class="bg-light text-center text-muted py-4 mt-5 transition-all">
<div class="container"> <div class="container">
<p>Générateur d'Exercices de Mathématiques &copy; 2025</p> <p>Générateur d'Exercices de Mathématiques &copy; 2025</p>
</div> </div>
@@ -248,6 +356,72 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
// Dark Mode Theme Management
class ThemeManager {
constructor() {
this.init();
}
init() {
// Load saved theme or detect system preference
const savedTheme = localStorage.getItem('theme');
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
const theme = savedTheme || systemTheme;
this.setTheme(theme);
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) {
this.setTheme(e.matches ? 'dark' : 'light');
}
});
// Setup toggle button
this.setupToggleButton();
}
setTheme(theme) {
document.documentElement.setAttribute('data-bs-theme', theme);
localStorage.setItem('theme', theme);
this.updateToggleIcon(theme);
}
toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-bs-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
this.setTheme(newTheme);
}
updateToggleIcon(theme) {
const icon = document.getElementById('darkModeIcon');
if (theme === 'dark') {
icon.className = 'bi bi-sun-fill';
} else {
icon.className = 'bi bi-moon-fill';
}
}
setupToggleButton() {
const toggleButton = document.getElementById('darkModeToggle');
toggleButton.addEventListener('click', () => {
this.toggleTheme();
this.animateToggle();
});
}
animateToggle() {
const button = document.getElementById('darkModeToggle');
button.style.transform = 'scale(0.9)';
setTimeout(() => {
button.style.transform = '';
}, 150);
}
}
// Initialize theme manager
const themeManager = new ThemeManager();
// Current page for pagination // Current page for pagination
let currentPage = 1; let currentPage = 1;
const pageSize = 10; const pageSize = 10;
@@ -258,7 +432,7 @@
minTable: document.getElementById('minTable').value, minTable: document.getElementById('minTable').value,
maxTable: document.getElementById('maxTable').value, maxTable: document.getElementById('maxTable').value,
numExercises: document.getElementById('numExercises').value, numExercises: document.getElementById('numExercises').value,
multiplicationOnly: document.getElementById('multiplicationOnly').checked, exerciseType: document.querySelector('input[name="exerciseType"]:checked').value,
maxFirstOperand: document.getElementById('maxFirstOperand').value maxFirstOperand: document.getElementById('maxFirstOperand').value
}; };
@@ -279,7 +453,18 @@
document.getElementById('minTable').value = exerciseValues.minTable || 1; document.getElementById('minTable').value = exerciseValues.minTable || 1;
document.getElementById('maxTable').value = exerciseValues.maxTable || 10; document.getElementById('maxTable').value = exerciseValues.maxTable || 10;
document.getElementById('numExercises').value = exerciseValues.numExercises || 15; document.getElementById('numExercises').value = exerciseValues.numExercises || 15;
document.getElementById('multiplicationOnly').checked = exerciseValues.multiplicationOnly || false;
// Restore radio button selection
if (exerciseValues.exerciseType) {
const radio = document.querySelector(`input[name="exerciseType"][value="${exerciseValues.exerciseType}"]`);
if (radio) {
radio.checked = true;
}
} else if (exerciseValues.multiplicationOnly !== undefined) {
// Handle legacy storage format
document.getElementById('multiplicationOnly').checked = exerciseValues.multiplicationOnly;
}
document.getElementById('maxFirstOperand').value = exerciseValues.maxFirstOperand || 10; document.getElementById('maxFirstOperand').value = exerciseValues.maxFirstOperand || 10;
} }
@@ -463,18 +648,25 @@
// Load PDF list on page load // Load PDF list on page load
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Ensure theme is initialized first
setTimeout(() => {
restoreFormValues(); restoreFormValues();
loadPdfList(currentPage); loadPdfList(currentPage);
}, 100);
}); });
// Add event listeners to save form values on change // Add event listeners to save form values on change
document.getElementById('minTable').addEventListener('change', saveFormValues); document.getElementById('minTable').addEventListener('change', saveFormValues);
document.getElementById('maxTable').addEventListener('change', saveFormValues); document.getElementById('maxTable').addEventListener('change', saveFormValues);
document.getElementById('numExercises').addEventListener('change', saveFormValues); document.getElementById('numExercises').addEventListener('change', saveFormValues);
document.getElementById('multiplicationOnly').addEventListener('change', saveFormValues);
document.getElementById('maxFirstOperand').addEventListener('change', saveFormValues); document.getElementById('maxFirstOperand').addEventListener('change', saveFormValues);
document.getElementById('numOperationExercises').addEventListener('change', saveFormValues); document.getElementById('numOperationExercises').addEventListener('change', saveFormValues);
// Add event listeners for radio buttons
document.querySelectorAll('input[name="exerciseType"]').forEach(radio => {
radio.addEventListener('change', saveFormValues);
});
// Event listener for select all button // Event listener for select all button
document.getElementById('selectAllBtn').addEventListener('click', function() { document.getElementById('selectAllBtn').addEventListener('click', function() {
document.querySelectorAll('.file-checkbox').forEach(checkbox => { document.querySelectorAll('.file-checkbox').forEach(checkbox => {
@@ -577,7 +769,8 @@
const minTable = parseInt(document.getElementById('minTable').value); const minTable = parseInt(document.getElementById('minTable').value);
const maxTable = parseInt(document.getElementById('maxTable').value); const maxTable = parseInt(document.getElementById('maxTable').value);
const numExercises = parseInt(document.getElementById('numExercises').value); const numExercises = parseInt(document.getElementById('numExercises').value);
const multiplicationOnly = document.getElementById('multiplicationOnly').checked; const exerciseType = document.querySelector('input[name="exerciseType"]:checked').value;
const multiplicationOnly = (exerciseType === 'multiplication');
const maxFirstOperand = parseInt(document.getElementById('maxFirstOperand').value); const maxFirstOperand = parseInt(document.getElementById('maxFirstOperand').value);
const generateBtn = document.getElementById('generateBtn'); const generateBtn = document.getElementById('generateBtn');
+3 -3
View File
@@ -41,7 +41,7 @@ spec:
memory: "64Mi" memory: "64Mi"
cpu: "250m" cpu: "250m"
limits: limits:
memory: "128Mi" memory: "512Mi"
cpu: "500m" cpu: "500m"
# Liveness probe # Liveness probe
livenessProbe: livenessProbe:
@@ -57,8 +57,8 @@ spec:
httpGet: httpGet:
path: /health path: /health
port: 8000 port: 8000
initialDelaySeconds: 5 initialDelaySeconds: 2
periodSeconds: 10 periodSeconds: 3
timeoutSeconds: 3 timeoutSeconds: 3
failureThreshold: 3 failureThreshold: 3
# Environment variables from ConfigMap # Environment variables from ConfigMap
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: keda-add-ons-http-interceptor-proxy
name: keda-add-ons-http-interceptor-proxy
spec:
externalName: keda-add-ons-http-interceptor-proxy.keda
selector:
app: keda-add-ons-http-interceptor-proxy
type: ExternalName
status:
loadBalancer: {}
@@ -0,0 +1,24 @@
kind: HTTPScaledObject
apiVersion: http.keda.sh/v1alpha1
metadata:
name: math-exercises
spec:
hosts:
- "math-tables.cl1.parano.ch"
pathPrefixes:
- /
scaleTargetRef:
name: math-exercises-app
kind: Deployment
apiVersion: apps/v1
service: math-exercises-service
portName: http
replicas:
min: 0
max: 3
scaledownPeriod: 60
scalingMetric:
requestRate:
granularity: 1s
targetValue: 100
window: 1m
@@ -0,0 +1,51 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: math-tables
resources:
- ../production
- external-service.yaml
- http-scaled-object.yaml
patches:
- patch: |
[
{
"op": "remove",
"path": "/spec/replicas"
}
]
target:
group: apps
version: v1
kind: Deployment
name: math-exercises-app
- path: remove-pdb.yaml
- patch: |
[
{
"op": "replace",
"path": "/spec/rules/0/http/paths/0/backend/service/port/number",
"value": 8080
},
{
"op": "replace",
"path": "/spec/rules/0/http/paths/0/backend/service/name",
"value": "keda-add-ons-http-interceptor-proxy"
}
]
target:
group: networking.k8s.io
version: v1
kind: Ingress
name: math-exercises-ingress
- patch: |
[
{
"op": "replace",
"path": "/spec/internalTrafficPolicy",
"value": "Cluster"
}
]
target:
version: v1
kind: Service
name: math-exercises-service
@@ -0,0 +1,5 @@
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: math-exercises-pdb
$patch: delete
@@ -13,7 +13,7 @@ resources:
images: images:
- name: math-exercises - name: math-exercises
newName: harbor.cl1.parano.ch/library/math-exercice newName: harbor.cl1.parano.ch/library/math-exercice
newTag: 1.0.9 newTag: 1.4.1
# Production-specific labels # Production-specific labels
+25 -25
View File
@@ -1,32 +1,32 @@
annotated-doc==0.0.4
annotated-types==0.7.0 annotated-types==0.7.0
anyio==4.10.0 anyio==4.14.1
boto3==1.26.160 boto3==1.43.38
boto3-stubs==1.40.23 boto3-stubs==1.43.38
botocore==1.29.165 botocore==1.43.38
botocore-stubs==1.40.23 botocore-stubs==1.43.14
click==8.2.1 click==8.4.2
defusedxml==0.7.1 defusedxml==0.7.1
fastapi==0.116.1 fastapi==0.138.2
fonttools==4.59.2 fonttools==4.63.0
fpdf2==2.8.4 fpdf2==2.8.7
h11==0.16.0 h11==0.16.0
idna==3.10 idna==3.18
Jinja2==3.1.4 Jinja2==3.1.6
jmespath==1.0.1 jmespath==1.1.0
MarkupSafe==3.0.2 MarkupSafe==3.0.3
pillow==11.3.0 pillow==12.3.0
pydantic==2.11.7 pydantic==2.13.4
pydantic_core==2.33.2 pydantic_core==2.46.4
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
python-multipart==0.0.9 python-multipart==0.0.32
s3transfer==0.6.2 s3transfer==0.19.0
six==1.17.0 six==1.17.0
sniffio==1.3.1 sniffio==1.3.1
starlette==0.47.3 starlette==0.46.2
types-awscrt==0.27.6 types-awscrt==0.34.1
types-fpdf2==2.8.4.20250822 types-s3transfer==0.16.0
types-s3transfer==0.13.1 typing-inspection==0.4.2
typing-inspection==0.4.1
typing_extensions==4.15.0 typing_extensions==4.15.0
urllib3==1.26.20 urllib3==2.7.0
uvicorn==0.30.6 uvicorn==0.49.0