new modif

This commit is contained in:
corenthin-lebreton 2026-02-27 00:10:09 +01:00
parent 3cca2f6613
commit c499c452f0
11 changed files with 407 additions and 92 deletions

View File

@ -0,0 +1,56 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
/** GET /api/notifications — récupère les 30 dernières notifs de l'utilisateur connecté */
export async function GET() {
try {
const supabase = await createClient()
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) return NextResponse.json({ error: 'Non autorisé' }, { status: 401 })
const { data, error } = await (supabase as any)
.from('notifications')
.select('id, type, title, body, metadata, read, created_at')
.eq('user_id', user.id)
.order('created_at', { ascending: false })
.limit(30)
if (error) return NextResponse.json({ error: error.message }, { status: 500 })
return NextResponse.json({ notifications: data ?? [] })
} catch {
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })
}
}
/** PATCH /api/notifications marque des notifications comme lues
* Body : { ids: string[] } ou { all: true }
*/
export async function PATCH(request: NextRequest) {
try {
const supabase = await createClient()
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) return NextResponse.json({ error: 'Non autorisé' }, { status: 401 })
const body = await request.json()
const db = supabase as any
if (body.all) {
await db
.from('notifications')
.update({ read: true })
.eq('user_id', user.id)
.eq('read', false)
} else if (Array.isArray(body.ids) && body.ids.length > 0) {
await db
.from('notifications')
.update({ read: true })
.eq('user_id', user.id)
.in('id', body.ids)
}
return NextResponse.json({ success: true })
} catch {
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })
}
}

View File

@ -2,6 +2,7 @@
import { NextRequest, NextResponse } from 'next/server'
import { createClient, createAdminClient } from '@/lib/supabase/server'
import { calculateScore } from '@/lib/utils'
import { createNotification } from '@/lib/notifications'
export async function PATCH(
request: NextRequest,
@ -75,6 +76,28 @@ export async function PATCH(
}
}
// Notification quand la session est fermée
if (!is_active) {
const { data: quizInfo } = await (supabase as any)
.from('quizzes')
.select('title')
.eq('id', session.quiz_id)
.single()
const { count: completedCount } = await (createAdminClient() as any)
.from('student_participations')
.select('id', { count: 'exact', head: true })
.eq('session_id', id)
.eq('status', 'completed')
await createNotification({
userId: user.id,
type: 'session_ended',
body: `Session "${quizInfo?.title ?? 'Quiz'}" terminée — ${completedCount ?? 0} participant(s) au total.`,
metadata: { session_id: id },
})
}
return NextResponse.json({ success: true })
} catch (error) {
console.error('[sessions/toggle]', error)

View File

@ -2,6 +2,7 @@
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { nanoid } from 'nanoid'
import { createNotification } from '@/lib/notifications'
const MAX_RETRIES = 10
@ -70,6 +71,20 @@ export async function POST(request: NextRequest) {
)
}
// Récupérer le titre du quiz pour la notification
const { data: quizInfo } = await db
.from('quizzes')
.select('title')
.eq('id', quiz_id)
.single()
await createNotification({
userId: user.id,
type: 'session_created',
body: `Session "${quizInfo?.title ?? 'Quiz'}" créée — code : ${shortCode}`,
metadata: { session_id: session.id, short_code: shortCode },
})
return NextResponse.json({
success: true,
session: {

View File

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextRequest, NextResponse } from 'next/server'
import { createAdminClient } from '@/lib/supabase/server'
import { createNotification } from '@/lib/notifications'
export async function POST(request: NextRequest) {
try {
@ -65,6 +66,22 @@ export async function POST(request: NextRequest) {
)
}
// Récupérer trainer_id + titre du quiz pour la notification
const { data: sessionFull } = await db
.from('sessions')
.select('trainer_id, quiz:quizzes(title)')
.eq('id', session.id)
.single()
if (sessionFull?.trainer_id) {
await createNotification({
userId: sessionFull.trainer_id,
type: 'student_joined',
body: `${first_name.trim()} ${last_name.trim()} a rejoint "${(sessionFull.quiz as any)?.title ?? 'votre quiz'}"`,
metadata: { session_id: session.id, participation_id: participation.id },
})
}
return NextResponse.json({
success: true,
participation: {

View File

@ -8,6 +8,7 @@ import {
Zap,
ArrowRight,
FileText,
Bell,
} from 'lucide-react'
import Link from 'next/link'
import { cn } from '@/lib/utils'
@ -146,6 +147,15 @@ export default async function DashboardPage() {
const displayName = profile?.username ?? user?.email?.split('@')[0] ?? 'Formateur'
// 5 dernières notifications non lues pour la section du dashboard
const { data: recentNotifications } = await db
.from('notifications')
.select('id, type, title, body, created_at')
.eq('user_id', user!.id)
.eq('read', false)
.order('created_at', { ascending: false })
.limit(5)
const mappedSessions = (sessions ?? []).map((s: any) => ({
id: s.id,
title: s.quiz?.title ?? 'Quiz sans titre',
@ -206,23 +216,29 @@ export default async function DashboardPage() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6">
<div className="card p-6">
<h3 className="font-semibold text-text-primary mb-4">Notifications Récentes</h3>
<div className="space-y-4">
{[
{ icon: '🔧', title: 'Maintenance Système', desc: 'Prévue samedi à 22h. Sauvegardez votre travail.' },
{ icon: '✅', title: 'Session finalisée', desc: 'Les rapports sont disponibles pour JS Fundamentals.' },
{ icon: '👥', title: 'Nouvelles inscriptions', desc: '12 étudiants ont rejoint "React Basics Quiz".' },
].map((notif, i) => (
<div key={i} className="flex items-start gap-3">
<div className="w-8 h-8 bg-background-elevated rounded-lg flex items-center justify-center text-sm flex-shrink-0">
{notif.icon}
</div>
<div>
<p className="text-sm font-medium text-text-primary">{notif.title}</p>
<p className="text-xs text-text-muted">{notif.desc}</p>
</div>
</div>
))}
</div>
{(recentNotifications ?? []).length === 0 ? (
<div className="flex flex-col items-center justify-center py-6 text-center">
<Bell size={24} className="text-text-muted/40 mb-2" />
<p className="text-sm text-text-muted">Aucune nouvelle notification</p>
</div>
) : (
<div className="space-y-4">
{(recentNotifications ?? []).map((notif: any) => {
const icon = { session_created: '🚀', student_joined: '👤', session_ended: '✅' }[notif.type as string] ?? '🔔'
return (
<div key={notif.id} className="flex items-start gap-3">
<div className="w-8 h-8 bg-background-elevated rounded-lg flex items-center justify-center text-sm flex-shrink-0">
{icon}
</div>
<div>
<p className="text-sm font-medium text-text-primary">{notif.title}</p>
<p className="text-xs text-text-muted">{notif.body}</p>
</div>
</div>
)
})}
</div>
)}
</div>
<div className="card p-6 bg-gradient-to-br from-primary to-blue-700 border-0">

View File

@ -138,7 +138,7 @@ export default function QuizzesClient({ initialCategories, stats: initialStats }
if (!res.ok) {
setUploadError(data.error ?? "Erreur lors de l'import")
} else {
// Mettre à jour le chapitre dans le state local
// Mettre à jour le state local immédiatement
const newQuiz: Quiz = {
id: data.quiz.id,
title: data.quiz.title,
@ -154,11 +154,12 @@ export default function QuizzesClient({ initialCategories, stats: initialStats }
),
}))
)
if (data.quiz.replaced) {
// Compte inchangé (remplacement)
} else {
if (!data.quiz.replaced) {
setStats((s) => ({ ...s, totalQuizzes: s.totalQuizzes + 1 }))
}
// Invalide le cache du router pour que les prochaines navigations
// récupèrent les données fraîches depuis le serveur
router.refresh()
}
} catch {
setUploadError('Erreur réseau')
@ -213,6 +214,7 @@ export default function QuizzesClient({ initialCategories, stats: initialStats }
prev.map((c) => (c.id === id ? { ...c, name: editingCategoryName.trim() } : c))
)
setEditingCategoryId(null)
router.refresh()
}
} finally {
setSavingCategoryId(null)
@ -233,6 +235,7 @@ export default function QuizzesClient({ initialCategories, stats: initialStats }
totalQuizzes: Math.max(0, s.totalQuizzes - removedQuizzes),
}))
setConfirmDeleteCategoryId(null)
router.refresh()
}
} finally {
setDeletingCategoryId(null)
@ -259,6 +262,7 @@ export default function QuizzesClient({ initialCategories, stats: initialStats }
}))
)
setEditingSubchapterId(null)
router.refresh()
}
} finally {
setSavingSubchapterId(null)
@ -280,6 +284,7 @@ export default function QuizzesClient({ initialCategories, stats: initialStats }
)
setStats((s) => ({ ...s, totalQuizzes: Math.max(0, s.totalQuizzes - removedQuizzes) }))
setConfirmDeleteSubchapterId(null)
router.refresh()
}
} finally {
setDeletingSubchapterId(null)
@ -305,6 +310,7 @@ export default function QuizzesClient({ initialCategories, stats: initialStats }
)
setAddingSubchapterForCategoryId(null)
setNewSubchapterName('')
router.refresh()
}
} finally {
setSavingNewSubchapter(false)

View File

@ -3,6 +3,8 @@ import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'
import QuizzesClient from './QuizzesClient'
export const dynamic = 'force-dynamic'
export default async function QuizzesPage() {
try {
const supabase = await createClient()

View File

@ -12,6 +12,7 @@ export default async function CreateSessionPage() {
.from('quizzes')
.select(`id, title, questions(id), subchapter:subchapters(name, category:categories(name))`)
.eq('author_id', user!.id)
.not('subchapter_id', 'is', null)
.order('title')
return (

View File

@ -1,31 +1,84 @@
'use client'
import { useState, useRef, useEffect } from 'react'
import { Bell, X } from 'lucide-react'
import { useState, useRef, useEffect, useCallback } from 'react'
import { Bell, X, Check, Loader2 } from 'lucide-react'
import { createClient } from '@/lib/supabase/client'
import { cn } from '@/lib/utils'
interface Notification {
id: string
icon: string
type: string
title: string
desc: string
time: string
body: string
read: boolean
created_at: string
}
const INITIAL_NOTIFICATIONS: Notification[] = [
{ id: '1', icon: '✅', title: 'Session finalisée', desc: 'Rapports disponibles pour JS Fundamentals.', time: 'il y a 2h', read: false },
{ id: '2', icon: '👥', title: 'Nouvelles inscriptions', desc: '12 étudiants ont rejoint "React Basics Quiz".', time: 'il y a 4h', read: false },
{ id: '3', icon: '🔧', title: 'Maintenance prévue', desc: 'Samedi à 22h. Pensez à sauvegarder votre travail.', time: 'il y a 1j', read: true },
]
const typeIcon: Record<string, string> = {
session_created: '🚀',
student_joined: '👤',
session_ended: '✅',
}
function formatRelativeTime(dateStr: string): string {
const diff = Date.now() - new Date(dateStr).getTime()
const minutes = Math.floor(diff / 60_000)
if (minutes < 1) return "à l'instant"
if (minutes < 60) return `il y a ${minutes} min`
const hours = Math.floor(minutes / 60)
if (hours < 24) return `il y a ${hours}h`
const days = Math.floor(hours / 24)
return `il y a ${days}j`
}
export default function NotificationBell() {
const [open, setOpen] = useState(false)
const [notifications, setNotifications] = useState<Notification[]>(INITIAL_NOTIFICATIONS)
const [open, setOpen] = useState(false)
const [notifications, setNotifications] = useState<Notification[]>([])
const [loading, setLoading] = useState(true)
const ref = useRef<HTMLDivElement>(null)
const unreadCount = notifications.filter((n) => !n.read).length
// ── Fetch initial ────────────────────────────────────────────────────────
const fetchNotifications = useCallback(async () => {
try {
const res = await fetch('/api/notifications')
if (!res.ok) return
const data = await res.json()
setNotifications(data.notifications ?? [])
} catch {
// silencieux — la cloche reste vide plutôt que de crasher
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchNotifications()
}, [fetchNotifications])
// ── Supabase Realtime — nouvelles notifs en temps réel ───────────────────
useEffect(() => {
const supabase = createClient()
const channel = supabase
.channel('notifications-realtime')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'notifications' },
(payload) => {
const newNotif = payload.new as Notification
setNotifications((prev) => [newNotif, ...prev].slice(0, 30))
}
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [])
// ── Fermer le panneau au clic extérieur ──────────────────────────────────
useEffect(() => {
const handleClick = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
@ -34,16 +87,31 @@ export default function NotificationBell() {
return () => document.removeEventListener('mousedown', handleClick)
}, [])
const markRead = (id: string) =>
// ── Marquer comme lu ─────────────────────────────────────────────────────
const markRead = async (id: string) => {
setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n)))
await fetch('/api/notifications', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: [id] }),
})
}
const markAllRead = () => setNotifications((prev) => prev.map((n) => ({ ...n, read: true })))
const markAllRead = async () => {
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })))
await fetch('/api/notifications', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ all: true }),
})
}
return (
<div ref={ref} className="relative">
<button
onClick={() => setOpen((v) => !v)}
className="relative p-2.5 bg-background-card border border-border rounded-lg hover:border-border-light transition-colors"
aria-label="Notifications"
>
<Bell size={18} className="text-text-secondary" />
{unreadCount > 0 && (
@ -53,6 +121,7 @@ export default function NotificationBell() {
{open && (
<div className="absolute right-0 top-12 w-80 bg-background-card border border-border rounded-xl shadow-2xl shadow-black/50 z-50 overflow-hidden animate-in fade-in slide-in-from-top-2 duration-150">
{/* En-tête */}
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-text-primary text-sm">Notifications</h3>
@ -66,9 +135,9 @@ export default function NotificationBell() {
{unreadCount > 0 && (
<button
onClick={markAllRead}
className="text-xs text-primary hover:text-primary-light transition-colors"
className="text-xs text-primary hover:text-primary-light transition-colors flex items-center gap-1"
>
Tout lire
<Check size={11} /> Tout lire
</button>
)}
<button
@ -80,29 +149,41 @@ export default function NotificationBell() {
</div>
</div>
{/* Liste */}
<div className="max-h-80 overflow-y-auto">
{notifications.map((notif) => (
<div
key={notif.id}
onClick={() => markRead(notif.id)}
className={cn(
'flex items-start gap-3 px-4 py-3 hover:bg-background-elevated/50 transition-colors cursor-pointer border-b border-border/50 last:border-0',
!notif.read && 'bg-primary/5'
)}
>
<div className="w-8 h-8 bg-background-elevated rounded-lg flex items-center justify-center text-sm flex-shrink-0">
{notif.icon}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2 mb-0.5">
<p className="text-sm font-medium text-text-primary truncate">{notif.title}</p>
{!notif.read && <div className="w-1.5 h-1.5 bg-primary rounded-full flex-shrink-0" />}
</div>
<p className="text-xs text-text-muted leading-relaxed">{notif.desc}</p>
<p className="text-xs text-text-muted/50 mt-1">{notif.time}</p>
</div>
{loading ? (
<div className="flex items-center justify-center py-8">
<Loader2 size={20} className="animate-spin text-text-muted" />
</div>
))}
) : notifications.length === 0 ? (
<div className="py-10 text-center">
<Bell size={28} className="mx-auto text-text-muted/40 mb-2" />
<p className="text-sm text-text-muted">Aucune notification</p>
</div>
) : (
notifications.map((notif) => (
<div
key={notif.id}
onClick={() => !notif.read && markRead(notif.id)}
className={cn(
'flex items-start gap-3 px-4 py-3 hover:bg-background-elevated/50 transition-colors border-b border-border/50 last:border-0',
!notif.read ? 'bg-primary/5 cursor-pointer' : 'cursor-default'
)}
>
<div className="w-8 h-8 bg-background-elevated rounded-lg flex items-center justify-center text-sm flex-shrink-0">
{typeIcon[notif.type] ?? '🔔'}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2 mb-0.5">
<p className="text-sm font-medium text-text-primary truncate">{notif.title}</p>
{!notif.read && <div className="w-1.5 h-1.5 bg-primary rounded-full flex-shrink-0" />}
</div>
<p className="text-xs text-text-muted leading-relaxed">{notif.body}</p>
<p className="text-xs text-text-muted/50 mt-1">{formatRelativeTime(notif.created_at)}</p>
</div>
</div>
))
)}
</div>
</div>
)}

44
lib/notifications.ts Normal file
View File

@ -0,0 +1,44 @@
import { createAdminClient } from '@/lib/supabase/server'
export type NotificationType =
| 'session_created'
| 'student_joined'
| 'session_ended'
const typeConfig: Record<NotificationType, { icon: string; defaultTitle: string }> = {
session_created: { icon: '🚀', defaultTitle: 'Session créée' },
student_joined: { icon: '👤', defaultTitle: 'Étudiant rejoint' },
session_ended: { icon: '✅', defaultTitle: 'Session terminée' },
}
interface CreateNotificationOptions {
userId: string
type: NotificationType
title?: string
body: string
metadata?: Record<string, unknown>
}
/**
* Insère une notification en base pour un utilisateur.
* Utilise le client admin (service_role) pour bypasser RLS sur l'INSERT.
* Les erreurs sont loggées mais n'interrompent pas le flux appelant.
*/
export async function createNotification(opts: CreateNotificationOptions): Promise<void> {
try {
const admin = createAdminClient() as any
const cfg = typeConfig[opts.type]
const { error } = await admin.from('notifications').insert({
user_id: opts.userId,
type: opts.type,
title: opts.title ?? cfg.defaultTitle,
body: opts.body,
metadata: opts.metadata ?? {},
})
if (error) console.error('[createNotification]', error.message)
} catch (err) {
console.error('[createNotification] unexpected error', err)
}
}

View File

@ -1,57 +1,111 @@
{
"title": "Introduction à Ansible",
"category": "Ansible",
"subchapter": "Introduction",
"title": "Control plane",
"questions": [
{
"question": "Qu'est-ce qu'Ansible ?",
"explanation": "Ansible est un outil d'automatisation IT open-source qui permet de configurer des systèmes, déployer des logiciels et orchestrer des tâches IT plus avancées.",
"question": "Le Scheduler et le Controller Manager accèdent directement à etcd pour lire l'état du cluster.",
"explanation": "C'est une règle d'or : seul le kube-apiserver communique directement avec etcd. Tous les autres composants passent par l'API Server pour obtenir ou modifier des informations.",
"answers": [
{ "text": "Un langage de programmation", "correct": false },
{ "text": "Un outil d'automatisation IT", "correct": true },
{ "text": "Un système de gestion de base de données", "correct": false },
{ "text": "Un serveur web", "correct": false }
{ "text": "Vrai", "correct": false },
{ "text": "Faux", "correct": true }
]
},
{
"question": "Quel fichier Ansible décrit les tâches à exécuter ?",
"explanation": "Un Playbook Ansible est un fichier YAML qui décrit une série de tâches à exécuter sur des hôtes distants.",
"question": "Quelles sont les trois fonctions principales de l'API Server ?",
"explanation": "L'API Server sert de hub central : il authentifie/valide les requêtes, stocke les données de manière persistante dans etcd et expose l'état actuel du cluster aux utilisateurs et composants.",
"answers": [
{ "text": "Inventory file", "correct": false },
{ "text": "Playbook", "correct": true },
{ "text": "Role", "correct": false },
{ "text": "Module", "correct": false }
{ "text": "Valider les requêtes, persister dans etcd, exposer l'état du cluster", "correct": true },
{ "text": "Scheduler les Pods, créer les ReplicaSets, monitorer les Nodes", "correct": false },
{ "text": "Lancer les conteneurs, assigner les Pods, gérer le réseau", "correct": false },
{ "text": "Authentifier les utilisateurs, chiffrer les secrets, gérer les certificats", "correct": false }
]
},
{
"question": "Dans quel format sont écrits les Playbooks Ansible ?",
"explanation": "Les Playbooks Ansible sont écrits en YAML (Yet Another Markup Language), un format lisible par l'humain.",
"question": "Quel composant du Control Plane détecte les Nodes en panne ?",
"explanation": "Le Node Controller (intégré au kube-controller-manager) est responsable de la surveillance des nœuds et de l'éviction des Pods si un nœud ne répond plus.",
"answers": [
{ "text": "JSON", "correct": false },
{ "text": "XML", "correct": false },
{ "text": "YAML", "correct": true },
{ "text": "TOML", "correct": false }
{ "text": "Node controller", "correct": true },
{ "text": "Job controller", "correct": false },
{ "text": "ReplicaSet controller", "correct": false },
{ "text": "Endpoints controller", "correct": false }
]
},
{
"question": "Comment Ansible se connecte-t-il aux nœuds gérés ?",
"explanation": "Ansible utilise SSH (Secure Shell) pour se connecter aux nœuds Linux/Unix gérés, sans nécessiter d'agent sur les nœuds cibles.",
"question": "Le kube-controller-manager communique directement avec etcd pour mettre à jour l'état du cluster.",
"explanation": "Comme pour le Scheduler, le Controller Manager doit envoyer des requêtes à l'API Server, qui se chargera ensuite d'écrire dans etcd.",
"answers": [
{ "text": "Via un agent installé sur chaque nœud", "correct": false },
{ "text": "Via SSH", "correct": true },
{ "text": "Via HTTP", "correct": false },
{ "text": "Via FTP", "correct": false }
{ "text": "Vrai", "correct": false },
{ "text": "Faux", "correct": true }
]
},
{
"question": "Qu'est-ce qu'un inventaire Ansible ?",
"explanation": "L'inventaire Ansible est un fichier qui liste les hôtes et les groupes d'hôtes sur lesquels Ansible peut exécuter des tâches.",
"question": "Quelles sont les deux étapes du processus de scheduling ?",
"explanation": "Le scheduler procède d'abord par 'Filtrage' (éliminer les nœuds incompatibles) puis par 'Scoring' (classer les nœuds restants pour choisir le meilleur).",
"answers": [
{ "text": "Une liste de modules disponibles", "correct": false },
{ "text": "Un fichier de configuration global", "correct": false },
{ "text": "La liste des hôtes gérés", "correct": true },
{ "text": "Un catalogue de rôles", "correct": false }
{ "text": "Création puis démarrage", "correct": false },
{ "text": "Filtrage puis scoring", "correct": true },
{ "text": "Validation puis persistence", "correct": false },
{ "text": "Assignation puis Exécution", "correct": false }
]
},
{
"question": "Si le Control Plane tombe, les Pods existants continuent de tourner sur les Worker Nodes.",
"explanation": "Le Control Plane gère l'orchestration. Si il disparaît, le cluster devient 'gelé' (pas de nouveaux Pods), mais le plan de données (les Workers) continue d'exécuter ce qui est déjà en place.",
"answers": [
{ "text": "Vrai", "correct": true },
{ "text": "Faux", "correct": false }
]
},
{
"question": "Le kube-controller-manager agit uniquement lorsquun utilisateur interagit avec le cluster.",
"explanation": "Faux. C'est une boucle de contrôle infinie (control loop) qui compare l'état désiré à l'état réel en permanence, même sans action humaine.",
"answers": [
{ "text": "Vrai", "correct": false },
{ "text": "Faux", "correct": true }
]
},
{
"question": "L'API Server est le seul composant du Control Plane qui peut lire et écrire dans etcd.",
"explanation": "C'est une architecture conçue pour la sécurité et la cohérence des données : l'API Server est le gardien d'etcd.",
"answers": [
{ "text": "Vrai", "correct": true },
{ "text": "Faux", "correct": false }
]
},
{
"question": "Quel champ du Pod le Scheduler remplit-il lors de l'assignation à un Node ?",
"explanation": "Le processus de scheduling consiste techniquement à mettre à jour le champ 'nodeName' dans la définition du Pod.",
"answers": [
{ "text": "nodeName", "correct": true },
{ "text": "NamePod", "correct": false },
{ "text": "NomDuPod", "correct": false },
{ "text": "NameOfThePod", "correct": false }
]
},
{
"question": "Dans quel ordre les composants interviennent-ils lors de la création d'un Deployment ?",
"explanation": "L'API Server reçoit l'ordre, les Controllers créent les objets (RS, Pods), le Scheduler choisit le nœud, et enfin le Kubelet exécute.",
"answers": [
{ "text": "Scheduler → API Server → Controller Manager → Kubelet", "correct": false },
{ "text": "Controller Manager → Scheduler → Kubelet → API Server", "correct": false },
{ "text": "Kubelet → Scheduler → API Server → Controller Manager", "correct": false },
{ "text": "API Server → Deployment Controller → ReplicaSet Controller → Scheduler → Kubelet", "correct": true }
]
},
{
"question": "Un seul controller manager peut gérer plusieurs types de contrôleurs.",
"explanation": "Le 'kube-controller-manager' est un binaire unique qui regroupe de nombreux contrôleurs (Node, Deployment, Namespace, etc.) pour simplifier la gestion.",
"answers": [
{ "text": "Vrai", "correct": true },
{ "text": "Faux", "correct": false }
]
},
{
"question": "Le kube-apiserver interagit directement avec les kubelets sur les nœuds",
"explanation": "Bien que l'API Server puisse initier des connexions (pour les logs ou l'exécution de commandes), c'est généralement le Kubelet qui surveille l'API Server pour recevoir ses instructions.",
"answers": [
{ "text": "Vrai", "correct": false },
{ "text": "Faux", "correct": true }
]
}
]
}
}