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
FROM python:3.13-slim AS builder
FROM python:3.14-slim AS builder
# Set working directory
WORKDIR /app
@@ -17,7 +17,7 @@ RUN --mount=type=cache,id=pip,target=/root/.cache/pip \
--target=/app/site-packages \
-r requirements.txt
FROM python:3.13-slim
FROM python:3.14-slim
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.responses import HTMLResponse, RedirectResponse, StreamingResponse
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
from pydantic import BaseModel, Field
from fpdf import FPDF
import logging
@@ -68,13 +68,13 @@ class MathExercisesPDF(FPDF):
class ExerciseRequest(BaseModel):
min_table: int
max_table: int
num_exercises: int = 15
num_exercises: int = Field(15, ge=1, le=100)
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):
num_exercises: int = 20
num_exercises: int = Field(20, ge=1, le=100)
def generate_exercises(
+204 -11
View File
@@ -5,17 +5,42 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<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 {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transition: 0.3s;
margin-bottom: 20px;
background-color: var(--bs-tertiary-bg);
border-color: var(--bs-border-color);
}
.card:hover {
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
}
.exercise-result {
background-color: #f8f9fa;
background-color: var(--bs-secondary-bg);
border-radius: 5px;
padding: 15px;
margin-top: 20px;
@@ -25,9 +50,81 @@
height: 3rem;
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>
</head>
<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="row justify-content-center">
<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>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="multiplicationOnly">
<label class="form-check-label" for="multiplicationOnly">Générer uniquement des multiplications</label>
<div class="mb-3">
<label class="form-label">Types d'exercices</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 class="mb-3">
@@ -240,7 +348,7 @@
</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">
<p>Générateur d'Exercices de Mathématiques &copy; 2025</p>
</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>
// 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
let currentPage = 1;
const pageSize = 10;
@@ -258,7 +432,7 @@
minTable: document.getElementById('minTable').value,
maxTable: document.getElementById('maxTable').value,
numExercises: document.getElementById('numExercises').value,
multiplicationOnly: document.getElementById('multiplicationOnly').checked,
exerciseType: document.querySelector('input[name="exerciseType"]:checked').value,
maxFirstOperand: document.getElementById('maxFirstOperand').value
};
@@ -279,7 +453,18 @@
document.getElementById('minTable').value = exerciseValues.minTable || 1;
document.getElementById('maxTable').value = exerciseValues.maxTable || 10;
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;
}
@@ -463,18 +648,25 @@
// Load PDF list on page load
document.addEventListener('DOMContentLoaded', function() {
restoreFormValues();
loadPdfList(currentPage);
// Ensure theme is initialized first
setTimeout(() => {
restoreFormValues();
loadPdfList(currentPage);
}, 100);
});
// Add event listeners to save form values on change
document.getElementById('minTable').addEventListener('change', saveFormValues);
document.getElementById('maxTable').addEventListener('change', saveFormValues);
document.getElementById('numExercises').addEventListener('change', saveFormValues);
document.getElementById('multiplicationOnly').addEventListener('change', saveFormValues);
document.getElementById('maxFirstOperand').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
document.getElementById('selectAllBtn').addEventListener('click', function() {
document.querySelectorAll('.file-checkbox').forEach(checkbox => {
@@ -577,7 +769,8 @@
const minTable = parseInt(document.getElementById('minTable').value);
const maxTable = parseInt(document.getElementById('maxTable').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 generateBtn = document.getElementById('generateBtn');
+3 -3
View File
@@ -41,7 +41,7 @@ spec:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
memory: "512Mi"
cpu: "500m"
# Liveness probe
livenessProbe:
@@ -57,8 +57,8 @@ spec:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
initialDelaySeconds: 2
periodSeconds: 3
timeoutSeconds: 3
failureThreshold: 3
# 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:
- name: math-exercises
newName: harbor.cl1.parano.ch/library/math-exercice
newTag: 1.0.9
newTag: 1.4.1
# Production-specific labels
+25 -25
View File
@@ -1,32 +1,32 @@
annotated-doc==0.0.4
annotated-types==0.7.0
anyio==4.10.0
boto3==1.26.160
boto3-stubs==1.40.23
botocore==1.29.165
botocore-stubs==1.40.23
click==8.2.1
anyio==4.14.1
boto3==1.43.38
boto3-stubs==1.43.38
botocore==1.43.38
botocore-stubs==1.43.14
click==8.4.2
defusedxml==0.7.1
fastapi==0.116.1
fonttools==4.59.2
fpdf2==2.8.4
fastapi==0.138.2
fonttools==4.63.0
fpdf2==2.8.7
h11==0.16.0
idna==3.10
Jinja2==3.1.4
jmespath==1.0.1
MarkupSafe==3.0.2
pillow==11.3.0
pydantic==2.11.7
pydantic_core==2.33.2
idna==3.18
Jinja2==3.1.6
jmespath==1.1.0
MarkupSafe==3.0.3
pillow==12.3.0
pydantic==2.13.4
pydantic_core==2.46.4
python-dateutil==2.9.0.post0
python-multipart==0.0.9
s3transfer==0.6.2
python-multipart==0.0.32
s3transfer==0.19.0
six==1.17.0
sniffio==1.3.1
starlette==0.47.3
types-awscrt==0.27.6
types-fpdf2==2.8.4.20250822
types-s3transfer==0.13.1
typing-inspection==0.4.1
starlette==0.46.2
types-awscrt==0.34.1
types-s3transfer==0.16.0
typing-inspection==0.4.2
typing_extensions==4.15.0
urllib3==1.26.20
uvicorn==0.30.6
urllib3==2.7.0
uvicorn==0.49.0