From c7a31c794a4d45873d9d5d22511b01058fbb4e9d Mon Sep 17 00:00:00 2001 From: Massiles Ghernaout <749-gm213204@users.noreply.www-apps.univ-lehavre.fr> Date: Mon, 9 Feb 2026 01:27:12 +0100 Subject: [PATCH 1/2] make the UI nicer and improve UX --- frontend/src/App.jsx | 257 +++++++++++++++++++++++++++++++++---------- todo.txt | 6 - 2 files changed, 201 insertions(+), 62 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8449566..30192d8 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -12,6 +12,10 @@ const MODES = { const MODE_KEYS = ["veryGentle", "gentle", "normal", "fast", "aggressive", "veryAggressive"]; const intervalLabel = (ms) => (ms >= 1000 ? `${ms / 1000}s` : `${ms}ms`); + +const CLUSTER_POLL_INTERVAL_MS = 5000; +const CLUSTER_COUNTDOWN_SECS = Math.ceil(CLUSTER_POLL_INTERVAL_MS / 1000); // for good animation/UX + const MODE_LABELS = { veryGentle: `Très gentil (${intervalLabel(MODES.veryGentle)})`, gentle: `Gentil (${intervalLabel(MODES.gentle)})`, @@ -42,9 +46,14 @@ function App() { const [autoRunning, setAutoRunning] = useState(false); const [jobs, setJobs] = useState([]); const [clusterState, setClusterState] = useState(null); + + // for good UX/annimations + const [clusterCountdown, setClusterCountdown] = useState(CLUSTER_COUNTDOWN_SECS); + const [sendingHash, setSendingHash] = useState(false); const sendManual = async () => { if (!hashInput) return; + setSendingHash(true); try { const data = await apiRequest("/hash/manual", { method: "POST", @@ -55,6 +64,8 @@ function App() { } catch (e) { console.error(e); alert("Erreur lors de l'envoi du hash"); + } finally { + setSendingHash(false); } }; @@ -98,23 +109,31 @@ function App() { }, []); useEffect(() => { - // Monitoring cluster async function pollCluster() { try { const state = await apiRequest("/cluster/state"); setClusterState(state); + setClusterCountdown(CLUSTER_COUNTDOWN_SECS); } catch (e) { console.error("cluster state error", e); } } pollCluster(); - let pollClusterId = setInterval(pollCluster, 5000); + const pollClusterId = setInterval(pollCluster, CLUSTER_POLL_INTERVAL_MS); + return () => clearInterval(pollClusterId); + }, []); - return () => { - clearInterval(pollClusterId) - } - }, []) + // Countdown display: tick every second (resets to CLUSTER_COUNTDOWN_SECS after 0) + useEffect(() => { + if (clusterState == null) return; + + // Update our count down every second. + const t = setInterval(() => { + setClusterCountdown((c) => (c <= 0 ? CLUSTER_COUNTDOWN_SECS : c - 1)); + }, 1000); + return () => clearInterval(t); + }, [clusterState]); // Envoi auto de hash aléatoires selon le mode @@ -171,26 +190,80 @@ function App() { return (
+

MD5 Swarm Dashboard

-
-

Envoi manuel de hash MD5

-
+
+

Envoi manuel de hash MD5

+
setHashInput(e.target.value)} - style={{ flex: 1, padding: "0.5rem" }} + disabled={sendingHash} + style={{ + flex: 1, + padding: "0.5rem 0.75rem", + borderRadius: "6px", + border: "1px solid #ccc", + fontSize: "1rem", + }} /> -
-
-

Mode automatique

+
+

Mode automatique

-
-

État du cluster

+
+

État du cluster

{clusterState ? (
-
+
Jobs en attente : {clusterState.pendingJobs}
Workers actifs : {clusterState.workerReplicas}
+
+ + Rafraîchissement du cluster dans {clusterCountdown} seconde{clusterCountdown !== 1 ? "s" : ""} +
{clusterState.scaler && (
) : ( -

Chargement de l'état du cluster…

+
+ + Chargement de l'état du cluster… +
)}
-
-

Résultats

+
+

Résultats

{jobs.length > 0 && ( -
+
- + Télécharge deux fichiers : hashes (inputs) et résultats (outputs), un élément par ligne, même ordre. + {jobs.some((j) => !j.result) && ( + + + {jobs.filter((j) => !j.result).length} job(s) en cours + + )}
)} {jobs.length === 0 ? ( -

Aucun job pour le moment.

+

Aucun job pour le moment.

) : ( - - - - - - - - - - - - {jobs.map((job) => ( - - - - - - +
+
IDHashStatutPlaintextTemps (ms)
{job.id.slice(0, 8)}…{job.hash} - {job.result - ? job.result.found - ? "Trouvé" - : "Échec" - : "En cours / en file"} - - {job.result?.plaintext ?? "-"} - - {job.result?.elapsedMs ?? "-"} -
+ + + + + + + - ))} - -
IDHashStatutPlaintextTemps (ms)
+ + + {jobs.map((job) => ( + + {job.id.slice(0, 8)}… + {job.hash} + + {job.result + ? job.result.found + ? "Trouvé" + : "Échec" + : "En cours / en file"} + + + {job.result?.plaintext ?? "-"} + + + {job.result?.elapsedMs ?? "-"} + + + ))} + + +
)}
diff --git a/todo.txt b/todo.txt index 9c4867b..d4dfe2d 100644 --- a/todo.txt +++ b/todo.txt @@ -1,9 +1,3 @@ -* make the UI a bit nicer, add timers and loaders, (ex; add the text "refreshing the cluster state -in 2seconds" to the view) - -* add an export button to export the inputs and the outputs of automatic hash generation and submitting, make the input and - output files be seperate this way the teacher can inject the inputs to his program and check if the outputs match - * implement the CI using gitlab tool chain. * review how we use async/await, and if we do really need those awaits everywhere ? -- GitLab From 7f6eef1c79f517bd25586140836f708c882387c6 Mon Sep 17 00:00:00 2001 From: Massiles Ghernaout <749-gm213204@users.noreply.www-apps.univ-lehavre.fr> Date: Mon, 9 Feb 2026 01:30:02 +0100 Subject: [PATCH 2/2] minor UI updates (css) --- frontend/src/App.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 30192d8..abfe5b6 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -354,7 +354,7 @@ function App() {
{jobs.some((j) => !j.result) && ( - + +