pantdipendra commited on
Commit
f177a19
·
verified ·
1 Parent(s): ccc396f
Files changed (1) hide show
  1. app.js +686 -0
app.js ADDED
@@ -0,0 +1,686 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ /* ------------ Data ------------ */
3
+ const PURPOSES = [
4
+ {
5
+ id: "research",
6
+ title: "Health Research (HRA)",
7
+ blurb: "Scientific knowledge.",
8
+ refs: [],
9
+ },
10
+ {
11
+ id: "quality",
12
+ title: "Quality Improvement",
13
+ blurb: "Improve your own service.",
14
+ refs: [],
15
+ },
16
+ {
17
+ id: "statistics",
18
+ title: "Statistics / Official",
19
+ blurb: "Org/regional/national stats.",
20
+ refs: [],
21
+ },
22
+ {
23
+ id: "innovation",
24
+ title: "Innovation / AI",
25
+ blurb: "Tool/algorithm/model.",
26
+ refs: [],
27
+ },
28
+ {
29
+ id: "anon",
30
+ title: "Exclusively Anonymized Data",
31
+ blurb: "No reasonable reidentification risk.",
32
+ refs: [],
33
+ },
34
+ ];
35
+
36
+ const STEPS = [
37
+ {
38
+ num: 1,
39
+ key: "classification",
40
+ title: "Project classification",
41
+ items: [
42
+ { k: "1a", label: "(a) Confirm pathway.", req: true },
43
+ {
44
+ k: "1b",
45
+ label: "(b) Controller / joint controller — Art. 4(7).",
46
+ req: true,
47
+ },
48
+ { k: "1c", label: "(c) 1-page summary.", req: true },
49
+ { k: "1d", label: "(d) Special categories? — Art. 9.", req: true },
50
+ { k: "1e", label: "(e) Vulnerable groups screened.", req: true },
51
+ {
52
+ k: "1f",
53
+ label: "(f) DPIA screening / DPIA — Art. 35.",
54
+ req: true,
55
+ },
56
+ { k: "1g", label: "(g) Transfers (if any) — Chap. V.", req: false },
57
+ {
58
+ k: "1h",
59
+ label: "(h) Plan anonymization/pseudonymization — Recital 26.",
60
+ req: false,
61
+ },
62
+ ],
63
+ },
64
+ {
65
+ num: 2,
66
+ key: "legal",
67
+ title: "Legal basis & consent",
68
+ items: [
69
+ { k: "2a", label: "(a) Art. 6 lawful basis.", req: true },
70
+ { k: "2b", label: "(b) Art. 9(2) condition.", req: true },
71
+ {
72
+ k: "2d",
73
+ label: "(d) Purpose limitation — Art. 5(1)(b).",
74
+ req: true,
75
+ },
76
+ {
77
+ k: "2c",
78
+ label: "(c) Consent quality/withdrawal (if used).",
79
+ req: false,
80
+ },
81
+ {
82
+ k: "2e",
83
+ label: "(e) Research/statistics safeguards — Art. 89.",
84
+ req: false,
85
+ },
86
+ ],
87
+ },
88
+ {
89
+ num: 3,
90
+ key: "ethics",
91
+ title: "Approvals",
92
+ items: [
93
+ {
94
+ k: "3a",
95
+ label: "(a) Research: IRB/HRA §§2–4, §9, §33.",
96
+ req: true,
97
+ showFor: ["research"],
98
+ },
99
+ {
100
+ k: "3b",
101
+ label: "(b) Statistics: HRL §19–19h.",
102
+ req: true,
103
+ showFor: ["statistics"],
104
+ },
105
+ {
106
+ k: "3c",
107
+ label: "(c) QI/Other: internal governance.",
108
+ req: true,
109
+ showFor: ["quality", "other"],
110
+ },
111
+ {
112
+ k: "3d",
113
+ label: "(d) Innovation/AI: risk review / AI Act.",
114
+ req: true,
115
+ showFor: ["innovation"],
116
+ },
117
+ {
118
+ k: "3e",
119
+ label: "(e) RoPA + notices — Arts. 30, 13/14.",
120
+ req: true,
121
+ },
122
+ ],
123
+ },
124
+ {
125
+ num: 4,
126
+ key: "access",
127
+ title: "Data access & contracts",
128
+ items: [
129
+ { k: "4a", label: "(a) Access route confirmed.", req: true },
130
+ { k: "4b", label: "(b) DPA / sharing / DUA ready.", req: true },
131
+ { k: "4c", label: "(c) Minimize data — Art. 5(1)(c).", req: true },
132
+ { k: "4d", label: "(d) Transfers (if any) — Chap. V.", req: false },
133
+ ],
134
+ },
135
+ {
136
+ num: 5,
137
+ key: "security",
138
+ title: "Security & privacy",
139
+ items: [
140
+ { k: "5a", label: "(a) TOMs — Art. 32.", req: true },
141
+ { k: "5b", label: "(b) Pseudonymize; separate keys.", req: true },
142
+ { k: "5c", label: "(c) DPIA outcomes signed.", req: true },
143
+ { k: "5d", label: "(d) Breach plan — 33/34.", req: true },
144
+ ],
145
+ },
146
+ {
147
+ num: 6,
148
+ key: "quality",
149
+ title: "Data minimization and quality",
150
+ items: [
151
+ { k: "6a", label: "(a) Provenance & dictionary.", req: true },
152
+ {
153
+ k: "6b",
154
+ label: "(b) Validation / missing data plan.",
155
+ req: true,
156
+ },
157
+ ],
158
+ },
159
+ {
160
+ num: 7,
161
+ key: "analysis",
162
+ title: "Analysis & AI development",
163
+ items: [
164
+ { k: "7a", label: "(a) Extract matches approvals.", req: true },
165
+ {
166
+ k: "7c",
167
+ label: "(c) AI docs & bias checks (if AI).",
168
+ req: true,
169
+ showFor: ["innovation"],
170
+ },
171
+ ],
172
+ },
173
+ {
174
+ num: 8,
175
+ key: "compliance",
176
+ title: "Compliance monitoring and auditing",
177
+ items: [
178
+ { k: "8a", label: "(a) Internal audit.", req: true },
179
+ { k: "8b", label: "(b) Keep RoPA up to date.", req: true },
180
+ ],
181
+ },
182
+ {
183
+ num: 9,
184
+ key: "closeout",
185
+ title: "Dissemination, closeout, retention & deletion",
186
+ items: [
187
+ { k: "9a", label: "(a) Disclosure control OK.", req: true },
188
+ {
189
+ k: "9c",
190
+ label: "(c) Retention/deletion — Art. 5(1)(e).",
191
+ req: true,
192
+ },
193
+ ],
194
+ },
195
+ ];
196
+
197
+ /* Flow questions derived from the flowchart */
198
+ const FLOW = {
199
+ 1: {
200
+ q: "Project classification complete & documented?",
201
+ onYes: 2,
202
+ onNo: 1,
203
+ },
204
+ 2: { q: "Valid legal route and consent completed?", onYes: 3, onNo: 2 },
205
+ 3: {
206
+ q: "All ethical and regulatory approvals in place?",
207
+ onYes: 4,
208
+ onNo: 3,
209
+ },
210
+ 4: { q: "Data access route & contracts are ok?", onYes: 5, onNo: 4 },
211
+ 5: {
212
+ q: "Information security & privacy measures and risks controlled?",
213
+ onYes: 6,
214
+ onNo: 5,
215
+ },
216
+ 6: { q: "Proceed to analysis?", onYes: 7, onNo: 6 },
217
+ /* Step 7 has several conditional checks in order */
218
+ 7: [
219
+ { q: "Extract matches necessary approvals?", yes: 8, no: 3 },
220
+ { q: "Any new purpose?", yes: 1, no: "next" },
221
+ { q: "Any new approvals needed?", yes: 3, no: "next" },
222
+ { q: "Any new access/contracts?", yes: 4, no: 8 },
223
+ ],
224
+ 8: { q: "Any gaps found?", onYes: 4, onNo: 9 },
225
+ 9: { q: "Retain/reuse beyond plan?", onYes: 1, onNo: "end" },
226
+ };
227
+
228
+ /* ------------ State ------------ */
229
+ const STORAGE = "suhr_flow_v1";
230
+ const $ = (s) => document.querySelector(s);
231
+ const $$ = (s) => Array.from(document.querySelectorAll(s));
232
+ const state = {
233
+ purpose: null,
234
+ checks: {},
235
+ savedAt: null,
236
+ visible: 0,
237
+ step: 0,
238
+ subQ: 0,
239
+ reachedEnd: false, // track if user ever reached the end screen
240
+ };
241
+
242
+ /* ------------ Purpose UI ------------ */
243
+ function renderPurposes() {
244
+ const host = $("#purposeList");
245
+ host.innerHTML = "";
246
+ PURPOSES.forEach((p) => {
247
+ const el = document.createElement("label");
248
+ el.className = "option";
249
+ el.innerHTML = `
250
+ <input type="radio" name="purpose" value="${p.id}">
251
+ <div style="display:flex;justify-content:space-between;gap:8px">
252
+ <div><b>${p.title}</b><div class="tiny" style="margin-top:4px">${
253
+ p.blurb
254
+ }</div></div>
255
+ <span class="tag gdpr">${
256
+ p.id === "anon" ? "No GDPR" : "GDPR"
257
+ }</span>
258
+ </div>`;
259
+ el.addEventListener("click", () => {
260
+ state.purpose = p.id;
261
+ save(true);
262
+ $("#anonMsg").classList.toggle("hidden", state.purpose !== "anon");
263
+ if (p.id === "anon") {
264
+ hideFlow();
265
+ renderDots(0);
266
+ $("#timeline").innerHTML = "";
267
+ $("#finish").classList.remove("show");
268
+ state.reachedEnd = false;
269
+ return;
270
+ }
271
+ // start at step 1
272
+ state.visible = 1;
273
+ state.step = 1;
274
+ state.subQ = 0;
275
+ state.reachedEnd = false;
276
+ save(true);
277
+ $("#finish").classList.remove("show");
278
+ renderDots(state.visible);
279
+ renderTimeline();
280
+ showFlow();
281
+ askCurrent(); // auto-expand step 1
282
+ });
283
+ host.appendChild(el);
284
+ });
285
+ if (state.purpose) {
286
+ $$('input[name="purpose"]').forEach((i) => {
287
+ if (i.value === state.purpose) {
288
+ i.checked = true;
289
+ i.closest(".option").classList.add("active");
290
+ }
291
+ });
292
+ $("#anonMsg").classList.toggle("hidden", state.purpose !== "anon");
293
+ }
294
+ }
295
+
296
+ /* ------------ Timeline (accordion) ------------ */
297
+ function renderTimeline() {
298
+ const host = $("#timeline");
299
+ host.innerHTML = "";
300
+ if (!state.purpose || state.purpose === "anon") return;
301
+ const v = state.visible || 1;
302
+ for (let i = 0; i < v; i++) addStep(host, STEPS[i], i + 1);
303
+ expandDetailsForStep(state.step);
304
+ }
305
+
306
+ function addStep(host, step, idx) {
307
+ const done = isStepComplete(step);
308
+ const sec = document.createElement("section");
309
+ sec.className = "step" + (done ? " done" : "");
310
+ sec.dataset.step = step.key;
311
+ sec.innerHTML = `
312
+ <div class="head">
313
+ <div class="title">
314
+ <span class="num">${step.num}</span>
315
+ <div><b>${step.title}</b></div>
316
+ </div>
317
+ <div style="display:flex;gap:8px;align-items:center">
318
+ <span class="badge" id="badge_${step.key}">${
319
+ done ? "Complete ✓" : "In progress"
320
+ }</span>
321
+ <button class="toggle" data-fold="${step.key}">Details</button>
322
+ </div>
323
+ </div>
324
+ <div class="body fold" id="fold_${step.key}">
325
+ <ul class="list"></ul>
326
+ </div>`;
327
+ const ul = sec.querySelector(".list");
328
+ step.items.forEach((it) => {
329
+ if (it.showFor && !it.showFor.includes(state.purpose)) return;
330
+ const checked = !!state.checks[step.key]?.[it.k];
331
+ const li = document.createElement("li");
332
+ li.className = "li";
333
+ const id = `${step.key}_${it.k}`;
334
+ li.innerHTML = `
335
+ <div class="letters">${it.k}</div>
336
+ <label for="${id}">${it.label}${
337
+ it.req
338
+ ? ` <span class='req'>★</span>`
339
+ : " <span class='opt'>(opt)</span>"
340
+ }</label>
341
+ <div style="display:flex;align-items:center;gap:8px">
342
+ <input class="chk" type="checkbox" id="${id}" ${
343
+ checked ? "checked" : ""
344
+ }/>
345
+ <svg class="box" viewBox="0 0 24 24" aria-hidden="true"><path class="tick" d="M5 13l4 4L19 7"/></svg>
346
+ </div>`;
347
+ li.querySelector(".chk").addEventListener("change", (e) => {
348
+ (state.checks[step.key] ??= {})[it.k] = e.target.checked;
349
+ save(true);
350
+
351
+ const nowDone = isStepComplete(step);
352
+ $("#badge_" + step.key).textContent = nowDone
353
+ ? "Complete ✓"
354
+ : "In progress";
355
+ if (nowDone && !sec.classList.contains("done")) {
356
+ sec.classList.add("done");
357
+ if (idx === 1) advanceToStep(2); // auto show Step 2 when Step 1 completes
358
+ } else if (!nowDone) {
359
+ sec.classList.remove("done");
360
+ }
361
+
362
+ renderDots(state.visible); // refresh top dots' done state
363
+ updateProgress();
364
+
365
+ // If the end screen is visible (or was reached before), update/show its message when all steps are complete.
366
+ if ($("#finish").classList.contains("show")) {
367
+ refreshFinishBanner();
368
+ } else if (state.reachedEnd && allStepsComplete()) {
369
+ finish();
370
+ }
371
+ });
372
+ ul.appendChild(li);
373
+ });
374
+
375
+ // "Details" toggle
376
+ sec.querySelector(".toggle").addEventListener("click", () => {
377
+ const pane = document.getElementById("fold_" + step.key);
378
+ pane.classList.toggle("fold");
379
+ });
380
+
381
+ host.appendChild(sec);
382
+ }
383
+
384
+ /* Expand current step details and collapse others while asking each question */
385
+ function expandDetailsForStep(stepNum) {
386
+ if (!stepNum) return;
387
+ // collapse all
388
+ $$("#timeline .body").forEach((pane) => pane.classList.add("fold"));
389
+ const current = STEPS[stepNum - 1];
390
+ if (!current) return;
391
+ const pane = document.getElementById("fold_" + current.key);
392
+ if (pane) pane.classList.remove("fold");
393
+ }
394
+
395
+ /* ------------ Dots / progress ------------ */
396
+ function renderDots(visible) {
397
+ const d = $("#dots");
398
+ d.innerHTML = "";
399
+ for (let i = 1; i <= STEPS.length; i++) {
400
+ const stepObj = STEPS[i - 1];
401
+ const stepDone = isStepComplete(stepObj);
402
+ const dot = document.createElement("div");
403
+ dot.className =
404
+ "dot" +
405
+ (i <= visible ? " unlocked" : "") +
406
+ (i === state.step ? " active" : "") +
407
+ (stepDone ? " done" : "");
408
+ dot.textContent = i;
409
+ dot.setAttribute(
410
+ "aria-label",
411
+ `Step ${i}${stepDone ? " complete" : ""}`
412
+ );
413
+ if (i <= visible) {
414
+ dot.addEventListener("click", () => {
415
+ state.step = i;
416
+ state.subQ = 0;
417
+ save(true);
418
+ askCurrent();
419
+ scrollToStep(i);
420
+ highlightDot();
421
+ });
422
+ }
423
+ d.appendChild(dot);
424
+ }
425
+ updateProgress();
426
+ }
427
+ function highlightDot() {
428
+ $$("#dots .dot").forEach((el, idx) => {
429
+ el.classList.toggle("active", idx + 1 === state.step);
430
+ });
431
+ }
432
+ function scrollToStep(n) {
433
+ const el = document.querySelector(`.step:nth-of-type(${n})`);
434
+ if (!el) return;
435
+ el.scrollIntoView({ behavior: "smooth", block: "start" });
436
+ el.animate(
437
+ [
438
+ { outlineColor: "#56b4e9", outlineWidth: "0px" },
439
+ { outlineColor: "#56b4e9", outlineWidth: "6px" },
440
+ { outlineColor: "transparent", outlineWidth: "0px" },
441
+ ],
442
+ { duration: 800 }
443
+ );
444
+ }
445
+
446
+ /* ------------ Flow controller (Yes/No) ------------ */
447
+ const flow = $("#flow"),
448
+ qtxt = $("#qtxt"),
449
+ yesBtn = $("#yesBtn"),
450
+ noBtn = $("#noBtn");
451
+
452
+ function showFlow() {
453
+ flow.classList.remove("hidden");
454
+ }
455
+ function hideFlow() {
456
+ flow.classList.add("hidden");
457
+ }
458
+
459
+ function askCurrent() {
460
+ // Reset finish visibility text live region while navigating
461
+ $("#finish").classList.remove("show");
462
+
463
+ if (state.purpose === "anon" || !state.step) {
464
+ hideFlow();
465
+ return;
466
+ }
467
+
468
+ // Ensure the current step's checklist is expanded (and others collapsed)
469
+ expandDetailsForStep(state.step);
470
+
471
+ // Step 7 special sequence
472
+ if (state.step === 7) {
473
+ const seq = FLOW[7];
474
+ let idx = state.subQ;
475
+ if (idx >= seq.length) {
476
+ state.step = 8;
477
+ state.subQ = 0;
478
+ renderDots(state.visible);
479
+ askCurrent();
480
+ return;
481
+ }
482
+ const node = seq[idx];
483
+ qtxt.textContent = node.q;
484
+ yesBtn.onclick = () => {
485
+ if (node.yes === "next") {
486
+ state.subQ++;
487
+ askCurrent();
488
+ return;
489
+ }
490
+ goto(node.yes);
491
+ };
492
+ noBtn.onclick = () => {
493
+ if (node.no === "next") {
494
+ state.subQ++;
495
+ askCurrent();
496
+ return;
497
+ }
498
+ goto(node.no);
499
+ };
500
+ return;
501
+ }
502
+
503
+ // Regular step question
504
+ const node = FLOW[state.step];
505
+ if (!node) {
506
+ hideFlow();
507
+ return;
508
+ }
509
+ qtxt.textContent = node.q;
510
+ yesBtn.onclick = () => goto(node.onYes);
511
+ noBtn.onclick = () => goto(node.onNo);
512
+ }
513
+
514
+ function goto(dest) {
515
+ if (dest === "end") {
516
+ finish();
517
+ return;
518
+ }
519
+ // move
520
+ state.step = dest;
521
+ state.subQ = 0;
522
+ if (state.step > state.visible) state.visible = state.step; // grow visibility
523
+ renderDots(state.visible);
524
+ renderTimeline();
525
+ askCurrent();
526
+ scrollToStep(state.step);
527
+ save(true);
528
+ }
529
+
530
+ /* Called when step 1 completes to auto reveal step 2 */
531
+ function advanceToStep(n) {
532
+ if (state.visible < n) {
533
+ state.visible = n;
534
+ renderDots(state.visible);
535
+ renderTimeline();
536
+ scrollToStep(n);
537
+ }
538
+ state.step = n;
539
+ state.subQ = 0;
540
+ askCurrent();
541
+ save(true);
542
+ }
543
+
544
+ /* ------------ Helpers ------------ */
545
+ function isStepComplete(step) {
546
+ const d = state.checks[step.key] || {};
547
+ const req = step.items.filter(
548
+ (it) => !(it.showFor && !it.showFor.includes(state.purpose)) && it.req
549
+ );
550
+ return req.length ? req.every((it) => !!d[it.k]) : true;
551
+ }
552
+ function allStepsComplete() {
553
+ if (!state.purpose || state.purpose === "anon") return false;
554
+ return STEPS.every((s) => isStepComplete(s));
555
+ }
556
+ function updateProgress() {
557
+ let total = 0,
558
+ done = 0;
559
+ STEPS.forEach((s) => {
560
+ if (state.purpose === "anon" && s.num > 1) return;
561
+ s.items.forEach((it) => {
562
+ if (it.showFor && !it.showFor.includes(state.purpose)) return;
563
+ if (it.req) {
564
+ total++;
565
+ if (state.checks[s.key]?.[it.k]) done++;
566
+ }
567
+ });
568
+ });
569
+ const pct = total ? Math.round((100 * done) / total) : 0;
570
+ $("#pbar").style.width = pct + "%";
571
+ }
572
+
573
+ /* ------------ Finish ------------ */
574
+ function finish() {
575
+ hideFlow();
576
+ state.reachedEnd = true;
577
+ save(true);
578
+
579
+ const ok = allStepsComplete();
580
+ const finishEl = $("#finish");
581
+ const titleEl = $("#finishTitle");
582
+ const subEl = $("#finishSub");
583
+ const confettiHost = $("#confetti");
584
+
585
+ if (ok) {
586
+ titleEl.textContent = "End 🎉";
587
+ subEl.textContent = "All flow checks passed.";
588
+ confettiHost.innerHTML = "";
589
+ confetti(24);
590
+ } else {
591
+ titleEl.textContent = "End";
592
+ subEl.textContent = "";
593
+ confettiHost.innerHTML = ""; // no confetti if not fully complete
594
+ }
595
+ finishEl.classList.add("show");
596
+ }
597
+
598
+ function refreshFinishBanner() {
599
+ // Update finish banner live without toggling visibility
600
+ const ok = allStepsComplete();
601
+ const titleEl = $("#finishTitle");
602
+ const subEl = $("#finishSub");
603
+ const confettiHost = $("#confetti");
604
+
605
+ if (ok) {
606
+ titleEl.textContent = "End 🎉";
607
+ subEl.textContent = "All flow checks passed.";
608
+ confettiHost.innerHTML = "";
609
+ confetti(24);
610
+ } else {
611
+ titleEl.textContent = "End";
612
+ subEl.textContent = "";
613
+ confettiHost.innerHTML = "";
614
+ }
615
+ }
616
+
617
+ function confetti(n) {
618
+ const host = $("#confetti");
619
+ host.innerHTML = "";
620
+ for (let i = 0; i < n; i++) {
621
+ const piece = document.createElement("i");
622
+ piece.style.setProperty("--dx", Math.random() * 300 - 150 + "px");
623
+ piece.style.left = 20 + Math.random() * 60 + "%";
624
+ piece.style.background = i % 2 ? "#0072B2" : "#E69F00";
625
+ piece.style.animationDelay = Math.random() * 0.4 + "s";
626
+ host.appendChild(piece);
627
+ }
628
+ }
629
+
630
+ /* ------------ Save/Load ------------ */
631
+ function save(quiet = false) {
632
+ state.savedAt = Date.now();
633
+ localStorage.setItem(STORAGE, JSON.stringify(state));
634
+ if (!quiet) updateProgress();
635
+ }
636
+ function load() {
637
+ try {
638
+ const raw = localStorage.getItem(STORAGE);
639
+ if (raw) Object.assign(state, JSON.parse(raw));
640
+ } catch (e) {}
641
+ }
642
+
643
+ /* ------------ Clear / Reset ------------ */
644
+ function clearAll() {
645
+ try {
646
+ localStorage.removeItem(STORAGE);
647
+ } catch (e) {}
648
+ state.purpose = null;
649
+ state.checks = {};
650
+ state.savedAt = null;
651
+ state.visible = 0;
652
+ state.step = 0;
653
+ state.subQ = 0;
654
+ state.reachedEnd = false;
655
+
656
+ renderPurposes();
657
+ renderDots(0);
658
+ $("#timeline").innerHTML = "";
659
+ $("#finish").classList.remove("show");
660
+ $("#anonMsg").classList.add("hidden");
661
+ hideFlow();
662
+ updateProgress();
663
+ }
664
+
665
+ /* ------------ Boot ------------ */
666
+ load();
667
+ renderPurposes();
668
+ if (state.purpose && state.purpose !== "anon") {
669
+ state.visible = state.visible || 1;
670
+ state.step = state.step || 1;
671
+ renderDots(state.visible);
672
+ renderTimeline();
673
+ showFlow();
674
+ askCurrent();
675
+ } else {
676
+ hideFlow();
677
+ }
678
+ updateProgress();
679
+
680
+ // Clear button handler
681
+ document.getElementById("clearBtn").addEventListener("click", () => {
682
+ const ok = confirm(
683
+ "Clear all saved progress and do fresh start? This will remove your selections!"
684
+ );
685
+ if (ok) clearAll();
686
+ });