feat: ci: add Dockerfile, docker-compose, Traefik config and Gitea Actions workflows
Build & Push / build (push) Failing after 11s
Traefik Config / traefik (push) Has been cancelled

This commit is contained in:
Julien Denizot
2026-04-13 18:31:37 +02:00
parent 6c092dc014
commit 7f7c095015
41 changed files with 1302 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
name: Build & Push
on:
push:
branches: [main]
jobs:
build:
runs-on: [build, docker, rust]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login Gitea Registry
run: |
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.enuxia.fr \
-u ${{ gitea.actor }} --password-stdin
- name: Setup Docker Buildx
run: |
docker buildx create --use --name multiarch || true
- name: Build & Push ARM64
run: |
docker buildx build \
--platform linux/arm64 \
--tag git.enuxia.fr/enuxia-public/enuxia-quiz:latest \
--tag git.enuxia.fr/enuxia-public/enuxia-quiz:${{ gitea.sha }} \
--push \
.
- name: Logout
if: always()
run: docker logout git.enuxia.fr
+30
View File
@@ -0,0 +1,30 @@
name: Deploy Pi
on:
workflow_run:
workflows: ["Build & Push"]
types: [completed]
jobs:
deploy:
runs-on: [deploy-app]
if: ${{ gitea.event.workflow_run.conclusion == 'success' }}
steps:
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H ${{ secrets.PI_HOST }} >> ~/.ssh/known_hosts
- name: Deploy sur le Pi
run: |
ssh -i ~/.ssh/deploy_key ${{ secrets.PI_USER }}@${{ secrets.PI_HOST }} \
"docker login git.enuxia.fr -u luuna -p ${{ secrets.REGISTRY_TOKEN }} && \
docker pull git.enuxia.fr/enuxia-public/enuxia-quiz:latest && \
docker compose -f /opt/enuxia-quiz/docker-compose.yml --env-file /opt/enuxia-quiz/.env up -d --force-recreate && \
docker logout git.enuxia.fr"
- name: Cleanup SSH
if: always()
run: rm -f ~/.ssh/deploy_key
+19
View File
@@ -0,0 +1,19 @@
name: Traefik Config
on:
push:
branches: [main]
paths:
- 'traefik/**'
jobs:
traefik:
runs-on: [deploy-traefik]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Deploy config Traefik
run: |
cp traefik/enuxia-quiz.yml /opt/gateway/traefik/dynamic/enuxia-quiz.yml
echo "✓ Config Traefik déployée"
@@ -0,0 +1,44 @@
{
"db_name": "SQLite",
"query": "SELECT\n q.id as question_id,\n q.text as question_text,\n q.section,\n COUNT(sa.id) as \"total_count: i64\",\n SUM(CASE WHEN sa.correct = 1 THEN 1 ELSE 0 END) as \"correct_count: i64\"\n FROM questions q\n LEFT JOIN student_answers sa ON sa.question_id = q.id\n LEFT JOIN submissions s ON s.id = sa.submission_id AND s.quiz_id = ?\n WHERE q.quiz_id = ?\n GROUP BY q.id\n ORDER BY q.position",
"describe": {
"columns": [
{
"name": "question_id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "question_text",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "section",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "total_count: i64",
"ordinal": 3,
"type_info": "Integer"
},
{
"name": "correct_count: i64",
"ordinal": 4,
"type_info": "Integer"
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "062cb95763326b6381b2d63523c5256a246a127d68829b94954782ac635ca256"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "DELETE FROM quizzes WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "117fa7176ad7e0f41f277783739c417272eaaabd5a855e67f0a7113a3fee5963"
}
@@ -0,0 +1,56 @@
{
"db_name": "SQLite",
"query": "SELECT id, quiz_id, first_name, last_name, score, total,\n submitted_at as \"submitted_at: chrono::DateTime<chrono::Utc>\"\n FROM submissions WHERE quiz_id = ?\n ORDER BY last_name, first_name",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "quiz_id",
"ordinal": 1,
"type_info": "Integer"
},
{
"name": "first_name",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "last_name",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "score",
"ordinal": 4,
"type_info": "Integer"
},
{
"name": "total",
"ordinal": 5,
"type_info": "Integer"
},
{
"name": "submitted_at: chrono::DateTime<chrono::Utc>",
"ordinal": 6,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "1a8e18219acf70f6719777de288c63d7f7798a24652c219c53abbe7b1678bc42"
}
@@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "SELECT title FROM quizzes WHERE id = ?",
"describe": {
"columns": [
{
"name": "title",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "1af19b597fe1a5246338a2ad060389cf2d8224c7267c20e4ef20d8e6cd475aea"
}
@@ -0,0 +1,26 @@
{
"db_name": "SQLite",
"query": "SELECT\n EXISTS(\n SELECT 1 FROM submissions\n WHERE quiz_id = ?\n AND first_name = ?\n AND last_name = ?\n ) as existing,\n EXISTS(\n SELECT 1 FROM resets\n WHERE quiz_id = ?\n AND first_name = ?\n AND last_name = ?\n AND used = 0\n ) as has_reset",
"describe": {
"columns": [
{
"name": "existing",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "has_reset",
"ordinal": 1,
"type_info": "Integer"
}
],
"parameters": {
"Right": 6
},
"nullable": [
false,
false
]
},
"hash": "30364604e9c6881a3f3db4637e6a302e3c270f9612175bf8b494fbeb3f56b0ae"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO resets (quiz_id, first_name, last_name) VALUES (?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "3de07cf64a7f17fc50ff39eb140daa0238124c94d81a8e295785909cecb1039a"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE questions SET text = ?, section = ?, position = ? WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "4d4aa56417a1b25dfe70ebad01085b46d2c2c0a1d6cc24de9fced1725dde4697"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO questions (quiz_id, text, section, position) VALUES (?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "50fadd296fda903f62d3adaa9e42db120aa267fde24595994f5f9136ec164a92"
}
@@ -0,0 +1,44 @@
{
"db_name": "SQLite",
"query": "SELECT id, quiz_id, first_name, last_name, used as \"used: bool\"\n FROM resets WHERE quiz_id = ? ORDER BY created_at DESC",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "quiz_id",
"ordinal": 1,
"type_info": "Integer"
},
{
"name": "first_name",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "last_name",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "used: bool",
"ordinal": 4,
"type_info": "Bool"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "56c3a62763c635c84ecbf468b074994103758c0903a25ed9e69b0573617d4264"
}
@@ -0,0 +1,62 @@
{
"db_name": "SQLite",
"query": "SELECT id, title, description,\n active as \"active: bool\",\n shuffle_questions as \"shuffle_questions: bool\",\n shuffle_answers as \"shuffle_answers: bool\",\n time_limit_seconds,\n created_at as \"created_at: chrono::DateTime<chrono::Utc>\"\n FROM quizzes WHERE id = ?",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "title",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "active: bool",
"ordinal": 3,
"type_info": "Bool"
},
{
"name": "shuffle_questions: bool",
"ordinal": 4,
"type_info": "Bool"
},
{
"name": "shuffle_answers: bool",
"ordinal": 5,
"type_info": "Bool"
},
{
"name": "time_limit_seconds",
"ordinal": 6,
"type_info": "Integer"
},
{
"name": "created_at: chrono::DateTime<chrono::Utc>",
"ordinal": 7,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
true,
false
]
},
"hash": "583c153124bf780b5670eaca062a9758621502767612ffbcf761c78c6a04e3da"
}
@@ -0,0 +1,56 @@
{
"db_name": "SQLite",
"query": "SELECT id, quiz_id, first_name, last_name, score, total,\n submitted_at as \"submitted_at: chrono::DateTime<chrono::Utc>\"\n FROM submissions WHERE quiz_id = ? ORDER BY score DESC",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "quiz_id",
"ordinal": 1,
"type_info": "Integer"
},
{
"name": "first_name",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "last_name",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "score",
"ordinal": 4,
"type_info": "Integer"
},
{
"name": "total",
"ordinal": 5,
"type_info": "Integer"
},
{
"name": "submitted_at: chrono::DateTime<chrono::Utc>",
"ordinal": 6,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "59ad0d1eb3ef477661c3a78cae712174c11dbd1df7684c0ddf8e1c24dcc0c4cc"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE resets SET used = 1\n WHERE quiz_id = ?\n AND first_name = ?\n AND last_name = ?\n AND used = 0",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "6390d90322dd8e117398f7716133354c63fa08e9ae917dc976589ea6c7c4afcd"
}
@@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "SELECT correct as \"correct: bool\" FROM answers WHERE id = ?",
"describe": {
"columns": [
{
"name": "correct: bool",
"ordinal": 0,
"type_info": "Bool"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "66e52beba899bcc08d4cd1586aa5785e1f1e74832a9242d32a09e40cd052072d"
}
@@ -0,0 +1,56 @@
{
"db_name": "SQLite",
"query": "SELECT\n q.text as q_text,\n q.section,\n a.label as chosen_label,\n a.text as chosen_text,\n sa.correct as \"is_correct: bool\",\n correct_a.label as correct_label,\n correct_a.text as correct_text\n FROM student_answers sa\n JOIN questions q ON q.id = sa.question_id\n JOIN answers a ON a.id = sa.answer_id\n JOIN answers correct_a ON correct_a.question_id = sa.question_id\n AND correct_a.correct = 1\n WHERE sa.submission_id = ?\n ORDER BY q.position",
"describe": {
"columns": [
{
"name": "q_text",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "section",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "chosen_label",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "chosen_text",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "is_correct: bool",
"ordinal": 4,
"type_info": "Bool"
},
{
"name": "correct_label",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "correct_text",
"ordinal": 6,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
true,
true
]
},
"hash": "7291781f8aa6a64540c52f31dd1f7140fc847963f33098dc6f7bf6a9662aec48"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "DELETE FROM resets\n WHERE quiz_id = ?\n AND LOWER(TRIM(first_name)) = ?\n AND LOWER(TRIM(last_name)) = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "73c411eceb3040317ef4ba3db04fdd089452faca291d0780b59eef19a7910d98"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO student_answers (submission_id, question_id, answer_id, correct) VALUES (?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "81980500da33b7bfa75cc6e356f0064a45930edba049576eb5c82992784ebacc"
}
@@ -0,0 +1,62 @@
{
"db_name": "SQLite",
"query": "SELECT id, title, description,\n active as \"active: bool\",\n shuffle_questions as \"shuffle_questions: bool\",\n shuffle_answers as \"shuffle_answers: bool\",\n time_limit_seconds,\n created_at as \"created_at: chrono::DateTime<chrono::Utc>\"\n FROM quizzes WHERE active = 1 ORDER BY created_at DESC",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "title",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "active: bool",
"ordinal": 3,
"type_info": "Bool"
},
{
"name": "shuffle_questions: bool",
"ordinal": 4,
"type_info": "Bool"
},
{
"name": "shuffle_answers: bool",
"ordinal": 5,
"type_info": "Bool"
},
{
"name": "time_limit_seconds",
"ordinal": 6,
"type_info": "Integer"
},
{
"name": "created_at: chrono::DateTime<chrono::Utc>",
"ordinal": 7,
"type_info": "Text"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
false,
false,
false,
true,
false
]
},
"hash": "974ce29db7c6d8f4084614c9027fdbce151dbe01a1f99f5f62b230fc93599790"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "9c7521481bf857c51d7c395753e8fef96e74e419440b43ef5109d2914f309fca"
}
@@ -0,0 +1,86 @@
{
"db_name": "SQLite",
"query": "SELECT\n sa.id as sa_id,\n sa.submission_id,\n sa.question_id,\n sa.answer_id,\n sa.correct as \"sa_correct: bool\",\n q.quiz_id,\n q.text as q_text,\n q.section,\n q.position,\n a.label,\n a.text as a_text,\n a.correct as \"a_correct: bool\"\n FROM student_answers sa\n JOIN questions q ON q.id = sa.question_id\n JOIN answers a ON a.id = sa.answer_id\n WHERE sa.submission_id = ?\n ORDER BY q.position",
"describe": {
"columns": [
{
"name": "sa_id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "submission_id",
"ordinal": 1,
"type_info": "Integer"
},
{
"name": "question_id",
"ordinal": 2,
"type_info": "Integer"
},
{
"name": "answer_id",
"ordinal": 3,
"type_info": "Integer"
},
{
"name": "sa_correct: bool",
"ordinal": 4,
"type_info": "Bool"
},
{
"name": "quiz_id",
"ordinal": 5,
"type_info": "Integer"
},
{
"name": "q_text",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "section",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "position",
"ordinal": 8,
"type_info": "Integer"
},
{
"name": "label",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "a_text",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "a_correct: bool",
"ordinal": 11,
"type_info": "Bool"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "9dd35b887ec6a0a00cd85a0acbb420a45449e6982170970bb66aeef233a7d225"
}
@@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "SELECT value FROM config WHERE key = ?",
"describe": {
"columns": [
{
"name": "value",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "9f446fea4be730b92077288cf55a73b6efc65c44156c0bb7eb22b382aacb890b"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "DELETE FROM questions WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "a23c3648497b6093974a2488be819ff7a99fc9d59c109594896ddecb19204192"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO submissions (quiz_id, first_name, last_name, score, total) VALUES (?, ?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "a52776015ba245b114124874a0e250a41396250ef5e73f987cafb88318700e35"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO quizzes (title, description) VALUES (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "a682287c0f20f270a0aa183f69c094fef56728ff6ea246d015e48942eeead7f4"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE quizzes\n SET title = ?, description = ?, active = ?,\n shuffle_questions = ?, shuffle_answers = ?,\n time_limit_seconds = ?\n WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "b5ad1b275532a5fee969cd4dec66e1f7e5d6c6c2bafeb1bbfc6e84f86ee43e9e"
}
@@ -0,0 +1,44 @@
{
"db_name": "SQLite",
"query": "SELECT id, quiz_id, text, section, position FROM questions WHERE quiz_id = ? ORDER BY position",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "quiz_id",
"ordinal": 1,
"type_info": "Integer"
},
{
"name": "text",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "section",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "position",
"ordinal": 4,
"type_info": "Integer"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "c47c6e7f9402abc300085c720014ba9e10e1a1203a3451cd74e9866c4c9489bc"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "DELETE FROM submissions\n WHERE quiz_id = ?\n AND LOWER(TRIM(first_name)) = ?\n AND LOWER(TRIM(last_name)) = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "c554b8748e1cf2d03da9ba6137ac2726775d94165b38e7a953b469acf3fa23c5"
}
@@ -0,0 +1,44 @@
{
"db_name": "SQLite",
"query": "SELECT id, question_id, label, text, correct as \"correct: bool\"\n FROM answers WHERE question_id = ? ORDER BY label",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "question_id",
"ordinal": 1,
"type_info": "Integer"
},
{
"name": "label",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "text",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "correct: bool",
"ordinal": 4,
"type_info": "Bool"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "c6596680247ad2ea057ebf95bffb9732d62f26782adc09c223f0fef8844eb577"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "DELETE FROM answers WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "cbf7717c5022a51f41241ed2aef99a916e4ff8c6c70e56b6f22633a0ed4e6bec"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO answers (question_id, label, text, correct) VALUES (?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "cec25b15cafd60cab31cb9a6c8907e0a4fbb657186adf02eae9ca31033225340"
}
@@ -0,0 +1,80 @@
{
"db_name": "SQLite",
"query": "SELECT\n sa.id as sa_id,\n sa.submission_id,\n sa.question_id,\n sa.answer_id,\n sa.correct as \"correct: bool\",\n q.text as question_text,\n q.section,\n q.position,\n q.quiz_id,\n a.label,\n a.text as answer_text\n FROM student_answers sa\n JOIN questions q ON q.id = sa.question_id\n JOIN answers a ON a.id = sa.answer_id\n WHERE sa.submission_id = ?\n ORDER BY q.position",
"describe": {
"columns": [
{
"name": "sa_id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "submission_id",
"ordinal": 1,
"type_info": "Integer"
},
{
"name": "question_id",
"ordinal": 2,
"type_info": "Integer"
},
{
"name": "answer_id",
"ordinal": 3,
"type_info": "Integer"
},
{
"name": "correct: bool",
"ordinal": 4,
"type_info": "Bool"
},
{
"name": "question_text",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "section",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "position",
"ordinal": 7,
"type_info": "Integer"
},
{
"name": "quiz_id",
"ordinal": 8,
"type_info": "Integer"
},
{
"name": "label",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "answer_text",
"ordinal": 10,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "cf3972c10a6ec56a774b76575abdab5eb9df8887fd5b214abf069d319882b79f"
}
@@ -0,0 +1,62 @@
{
"db_name": "SQLite",
"query": "SELECT id, title, description,\n active as \"active: bool\",\n shuffle_questions as \"shuffle_questions: bool\",\n shuffle_answers as \"shuffle_answers: bool\",\n time_limit_seconds,\n created_at as \"created_at: chrono::DateTime<chrono::Utc>\"\n FROM quizzes ORDER BY created_at DESC",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "title",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "active: bool",
"ordinal": 3,
"type_info": "Bool"
},
{
"name": "shuffle_questions: bool",
"ordinal": 4,
"type_info": "Bool"
},
{
"name": "shuffle_answers: bool",
"ordinal": 5,
"type_info": "Bool"
},
{
"name": "time_limit_seconds",
"ordinal": 6,
"type_info": "Integer"
},
{
"name": "created_at: chrono::DateTime<chrono::Utc>",
"ordinal": 7,
"type_info": "Text"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
false,
false,
false,
true,
false
]
},
"hash": "d399cb531bf1e4e0e4fd5eef1ed341974401adddda33d27ed81870a20d391597"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE answers SET label = ?, text = ?, correct = ? WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "dd1ada605f9406c7aab473cb9f8cc2e655968450f985882b9382d3bce738a99b"
}
@@ -0,0 +1,26 @@
{
"db_name": "SQLite",
"query": "SELECT\n EXISTS(\n SELECT 1 FROM submissions\n WHERE quiz_id = ?\n AND LOWER(TRIM(first_name)) = ?\n AND LOWER(TRIM(last_name)) = ?\n ) as existing,\n EXISTS(\n SELECT 1 FROM resets\n WHERE quiz_id = ?\n AND first_name = ?\n AND last_name = ?\n AND used = 0\n ) as has_reset",
"describe": {
"columns": [
{
"name": "existing",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "has_reset",
"ordinal": 1,
"type_info": "Integer"
}
],
"parameters": {
"Right": 6
},
"nullable": [
false,
false
]
},
"hash": "ea18f88dbfe1f03d56e14710420b2d817070c6508047902b3e2c7aef0d051134"
}
@@ -0,0 +1,56 @@
{
"db_name": "SQLite",
"query": "SELECT id, quiz_id, first_name, last_name, score, total,\n submitted_at as \"submitted_at: chrono::DateTime<chrono::Utc>\"\n FROM submissions WHERE id = ?",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "quiz_id",
"ordinal": 1,
"type_info": "Integer"
},
{
"name": "first_name",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "last_name",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "score",
"ordinal": 4,
"type_info": "Integer"
},
{
"name": "total",
"ordinal": 5,
"type_info": "Integer"
},
{
"name": "submitted_at: chrono::DateTime<chrono::Utc>",
"ordinal": 6,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "eec63079f7405e41a004aa8d23554a719e78c2df5e5bfd62f8f5d5a42d201243"
}
@@ -0,0 +1,56 @@
{
"db_name": "SQLite",
"query": "SELECT id, quiz_id, first_name, last_name, score, total,\n submitted_at as \"submitted_at: chrono::DateTime<chrono::Utc>\"\n FROM submissions WHERE quiz_id = ?\n ORDER BY submitted_at DESC",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "quiz_id",
"ordinal": 1,
"type_info": "Integer"
},
{
"name": "first_name",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "last_name",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "score",
"ordinal": 4,
"type_info": "Integer"
},
{
"name": "total",
"ordinal": 5,
"type_info": "Integer"
},
{
"name": "submitted_at: chrono::DateTime<chrono::Utc>",
"ordinal": 6,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "fe46285200e0d3a53ab65d413cf8e44fa4833b58431b6e385f9bc3f95c1b2cdd"
}
+64
View File
@@ -0,0 +1,64 @@
# ── Stage 1 : Builder ─────────────────────────────────────────────────────────
FROM --platform=linux/amd64 rust:1.85-slim AS builder
RUN apt-get update && apt-get install -y \
pkg-config \
libssl-dev \
curl \
git \
clang \
gcc-aarch64-linux-gnu \
libc6-dev-arm64-cross \
&& rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
RUN rustup target add wasm32-unknown-unknown
RUN rustup target add aarch64-unknown-linux-gnu
RUN cargo install cargo-leptos --locked
ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
ENV CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
COPY src ./src
COPY style ./style
COPY public ./public
COPY package*.json ./
COPY .sqlx ./.sqlx
RUN npm install
ENV LEPTOS_TAILWIND_VERSION=v4.1.13
ENV SQLX_OFFLINE=true
RUN cargo leptos build --release \
--bin-target-triple aarch64-unknown-linux-gnu
# ── Stage 2 : Runtime ─────────────────────────────────────────────────────────
FROM --platform=linux/arm64 debian:bookworm-slim AS runtime
RUN apt-get update && apt-get install -y \
ca-certificates \
libssl3 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /app/target/aarch64-unknown-linux-gnu/release/enuxia-quiz ./
COPY --from=builder /app/target/site ./site
RUN mkdir -p /data
ENV LEPTOS_OUTPUT_NAME=enuxia-quiz
ENV LEPTOS_SITE_ROOT=/app/site
ENV LEPTOS_SITE_ADDR=0.0.0.0:3000
ENV DATABASE_URL=sqlite:///data/quiz.db
EXPOSE 3000
CMD ["./enuxia-quiz"]
+13
View File
@@ -0,0 +1,13 @@
services:
enuxia-quiz:
image: git.enuxia.fr/enuxia-public/enuxia-quiz:latest
container_name: enuxia-quiz
restart: unless-stopped
ports:
- "3010:3000"
volumes:
- /opt/enuxia-quiz/data:/data
environment:
- DATABASE_URL=sqlite:///data/quiz.db
- SESSION_PASSWORD=${SESSION_PASSWORD}
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
+30
View File
@@ -0,0 +1,30 @@
http:
routers:
# ── Étudiants (public) ──────────────────────────────────────────────────
enuxia-quiz-public:
rule: "Host(`quiz.enuxia.fr`)"
entryPoints:
- websecure
middlewares:
- public-chain@file
service: enuxia-quiz-svc
tls:
certResolver: lehttp
# ── Admin (VPN uniquement) ──────────────────────────────────────────────
enuxia-quiz-admin:
rule: "Host(`admin-quiz.enuxia.fr`)"
entryPoints:
- websecure
middlewares:
- vpn-only@file
- sensitive-chain@file
service: enuxia-quiz-svc
tls:
certResolver: lehttp
services:
enuxia-quiz-svc:
loadBalancer:
servers:
- url: "http://100.91.166.9:3010"