Une séance peut être lancée en mode examen avec une durée dédiée. Réutilise le mécanisme d'expiration déjà en place (labs:expirer-sessions, expire_a) plutôt que d'en construire un nouveau. Le navigateur de l'élève signale les changements d'onglet et les sorties de plein écran (evenements_examen : un type et un horodatage, rien d'autre - pas de capture d'écran ni de webcam) ; le prof voit le compte par élève et le temps restant dans le cockpit de sa classe et la salle d'attente. Choix d'Adam après discussion des options (suivi chronométré simple / + détection d'onglet / + tentative de plein écran) : la troisième, honnêtement présentée - un navigateur ne permet pas de vraiment empêcher de sortir, seulement de le signaler. Avec cette étape, les quatre fonctions fictives du tableau de bord démo (quiz, devoirs, observations, examen) sont maintenant réelles. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
164 lines
10 KiB
PHP
164 lines
10 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('titre', "Salle d'attente")
|
|
|
|
@php($etats = \App\Models\LabSession::class)
|
|
|
|
@section('contenu')
|
|
<div class="mx-auto max-w-5xl px-5 py-14 lg:px-10">
|
|
<p class="font-mono text-sm font-semibold">{{ $seance->lab->titre }}</p>
|
|
@if ($seance->visioOuverte())
|
|
<p class="mt-3"><a href="{{ route('seances.visio.eleve', $seance) }}" class="inline-flex h-11 items-center rounded-full bg-[#0b57d0] px-5 font-bold text-white">Rejoindre la visio de la classe</a></p>
|
|
@endif
|
|
@if ($labSession->etat === $etats::EN_ATTENTE)
|
|
<div class="py-20 text-center">
|
|
<h1 class="text-4xl font-extrabold tracking-tight">En attente que le prof t'admette…</h1>
|
|
<p class="mt-4 font-semibold">Tes machines s'ouvriront ici dès qu'il t'aura admis.</p>
|
|
</div>
|
|
@elseif ($labSession->etat === $etats::ADMIS)
|
|
<div class="py-20 text-center">
|
|
<h1 class="text-4xl font-extrabold tracking-tight">Préparation de tes VM…</h1>
|
|
<p class="mt-4 font-semibold">Ça prend généralement moins d'une minute.</p>
|
|
</div>
|
|
@elseif ($labSession->etat === $etats::ECHEC)
|
|
<div class="py-20 text-center">
|
|
<h1 class="text-4xl font-extrabold tracking-tight">Tes machines n'ont pas pu être préparées</h1>
|
|
<p class="mt-4 font-semibold">Ton prof le voit et peut relancer. Cette page se mettra à jour toute seule.</p>
|
|
</div>
|
|
@elseif ($labSession->etat === $etats::ACTIF && $labSession->machines->isNotEmpty())
|
|
@if ($seance->mode_examen)
|
|
<div id="bandeau-examen" class="mb-4 flex flex-wrap items-center justify-between gap-3 rounded-2xl border border-[#A63D2A] bg-[#fce8e6] p-4">
|
|
<p class="font-extrabold">Mode examen · temps restant : <span id="temps-restant" class="font-mono">…</span></p>
|
|
<button type="button" id="bouton-plein-ecran" class="inline-flex h-10 items-center rounded-full bg-black px-4 font-bold text-white">Passer en plein écran</button>
|
|
</div>
|
|
<p class="mb-4 text-sm font-semibold text-[#5f6368]">Reste sur cette page : les changements d'onglet et les sorties du plein écran sont signalés à ton prof.</p>
|
|
@endif
|
|
<h1 class="mt-2 text-3xl font-extrabold tracking-tight">Tes machines</h1>
|
|
<div class="mt-4 flex gap-2" role="tablist">
|
|
@foreach ($labSession->machines as $i => $machine)
|
|
<button type="button" data-onglet="{{ $machine->id }}" class="onglet-machine rounded-lg border border-[#dadce0] px-4 py-2 font-mono font-bold {{ $i === 0 ? 'bg-[#0b57d0] text-white' : 'bg-white' }}">{{ $machine->nom }}</button>
|
|
@endforeach
|
|
</div>
|
|
@foreach ($labSession->machines as $i => $machine)
|
|
<div data-panneau="{{ $machine->id }}" @if ($i !== 0) hidden @endif class="mt-2 h-[480px] overflow-hidden rounded-2xl border border-[#e3e5e8]" style="background:#13233A">
|
|
<div id="terminal-{{ $machine->id }}" class="h-full"></div>
|
|
</div>
|
|
@endforeach
|
|
|
|
<section class="mt-6 rounded-2xl border border-[#e3e5e8] bg-white p-6" aria-labelledby="t-verification">
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
<h2 id="t-verification" class="text-2xl font-extrabold">Vérification</h2>
|
|
<form action="{{ route('lab-sessions.corriger', $labSession) }}" method="post" id="form-verifier">
|
|
@csrf
|
|
<button type="submit" class="inline-flex h-11 items-center rounded-full bg-black px-5 font-bold text-white">Vérifier mon travail</button>
|
|
</form>
|
|
</div>
|
|
@if (session('correction_indisponible'))
|
|
<p class="mt-4 rounded-xl bg-[#fce8e6] p-3 font-semibold">La vérification est indisponible pour le moment. Réessaie dans une minute.</p>
|
|
@endif
|
|
@if ($correction = $labSession->derniereCorrection)
|
|
<p class="mt-4 text-lg font-extrabold">
|
|
{{ $correction->reussi ? 'Réussi' : 'Pas encore réussi' }}
|
|
<span class="font-mono">· {{ $correction->note }}/{{ $correction->note_max }}</span>
|
|
</p>
|
|
<ul class="mt-3 grid gap-2">
|
|
@foreach ($correction->details as $detail)
|
|
<li class="rounded-xl border border-[#e3e5e8] p-3">
|
|
<span class="font-mono font-bold">{{ $detail['reussi'] ? '✓' : '✗' }}</span>
|
|
<span class="font-bold">{{ $detail['titre'] }}</span>
|
|
<span class="font-mono text-sm">({{ $detail['points'] }} pt{{ $detail['points'] > 1 ? 's' : '' }})</span>
|
|
@if ($detail['message'])
|
|
<p class="mt-1 font-semibold text-[#A63D2A]">{{ $detail['message'] }}</p>
|
|
@endif
|
|
</li>
|
|
@endforeach
|
|
</ul>
|
|
@endif
|
|
</section>
|
|
|
|
@vite('resources/js/console.js')
|
|
<script nonce="{{ Vite::cspNonce() }}" src="{{ asset('js/console-reelle.js') }}"></script>
|
|
<script nonce="{{ Vite::cspNonce() }}">
|
|
// Après les modules Vite (différés) : window.Terminal est défini.
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
document.querySelectorAll('.onglet-machine').forEach(function (bouton) {
|
|
bouton.addEventListener('click', function () {
|
|
var id = bouton.dataset.onglet;
|
|
document.querySelectorAll('.onglet-machine').forEach(function (b) { b.classList.toggle('bg-[#0b57d0]', b === bouton); b.classList.toggle('text-white', b === bouton); b.classList.toggle('bg-white', b !== bouton); });
|
|
document.querySelectorAll('[data-panneau]').forEach(function (p) { p.hidden = p.dataset.panneau !== id; });
|
|
});
|
|
});
|
|
document.getElementById('form-verifier').addEventListener('submit', function (e) {
|
|
var bouton = e.target.querySelector('button');
|
|
bouton.disabled = true;
|
|
bouton.textContent = 'Vérification en cours…';
|
|
});
|
|
@foreach ($labSession->machines as $machine)
|
|
fetch('{{ route('console.eleve', [$labSession, $machine]) }}', {
|
|
method: 'POST',
|
|
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
|
}).then(function (r) { return r.json(); }).then(function (d) {
|
|
window.SyllabusConsoleReelle.mount(document.getElementById('terminal-{{ $machine->id }}'), { jeton: d.jeton, lectureSeule: false, reveil: true });
|
|
});
|
|
@endforeach
|
|
});
|
|
</script>
|
|
@if ($seance->mode_examen)
|
|
<script nonce="{{ Vite::cspNonce() }}">
|
|
(function () {
|
|
var finA = new Date(@json($labSession->expire_a?->toIso8601String()));
|
|
var URL_EVENEMENT = @json(route('lab-sessions.evenements-examen', $labSession));
|
|
var CSRF = @json(csrf_token());
|
|
|
|
function signaler(type) {
|
|
fetch(URL_EVENEMENT, {
|
|
method: 'POST', keepalive: true,
|
|
headers: { 'X-CSRF-TOKEN': CSRF, 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
body: JSON.stringify({ type: type }),
|
|
}).catch(function () {});
|
|
}
|
|
|
|
function mettreAJourTemps() {
|
|
var restantMs = finA - new Date();
|
|
var affichage = document.getElementById('temps-restant');
|
|
if (restantMs <= 0) { affichage.textContent = '0 min'; location.reload(); return; }
|
|
affichage.textContent = Math.ceil(restantMs / 60000) + ' min';
|
|
}
|
|
mettreAJourTemps();
|
|
setInterval(mettreAJourTemps, 10000);
|
|
|
|
document.getElementById('bouton-plein-ecran').addEventListener('click', function () {
|
|
if (document.documentElement.requestFullscreen) document.documentElement.requestFullscreen().catch(function () {});
|
|
});
|
|
|
|
document.addEventListener('visibilitychange', function () {
|
|
if (document.hidden) signaler('changement_onglet');
|
|
});
|
|
document.addEventListener('fullscreenchange', function () {
|
|
if (!document.fullscreenElement) signaler('sortie_plein_ecran');
|
|
});
|
|
})();
|
|
</script>
|
|
@endif
|
|
@else
|
|
<div class="py-20 text-center">
|
|
<h1 class="text-4xl font-extrabold tracking-tight">Cette séance est terminée pour toi</h1>
|
|
<p class="mt-4 font-semibold">Tes machines ont été supprimées.</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
@if (in_array($labSession->etat, [$etats::EN_ATTENTE, $etats::ADMIS, $etats::ECHEC], true))
|
|
<script nonce="{{ Vite::cspNonce() }}">
|
|
(function () {
|
|
var etatAffiche = @json($labSession->etat);
|
|
setInterval(function () {
|
|
fetch('{{ route('seances.eleve.etat', $seance) }}', { headers: { 'Accept': 'application/json' } })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (d) { if (d.etat !== etatAffiche) location.reload(); })
|
|
.catch(function () {});
|
|
}, 1000);
|
|
})();
|
|
</script>
|
|
@endif
|
|
@endsection
|