PageSourceSearch

https://orbis.restaurant/personal/signup/signup.js?v=2

js orbis.restaurant collected 2026-09-30 05:44:49 UTC 29,045 bytes, 728 lines download raw bytes

1/* ============================================================
2   ORBIS Personal signup - multi-step state machine
3   ============================================================
4   Screens 1-11: identity + 10 anketa questions
5   Screen 'reading': pause animation between submit and OTP
6   Screen 'otp':     6-digit WhatsApp OTP
7   Screen 'email':   confirmation link with polling
8
9   Backend contract:
10     POST /api/personal/signup        body: STATE.data, returns { manager_id, already_existed, whatsapp_verified, email_verified }
11     POST /api/personal/otp/request   body: { whatsapp_number }
12     POST /api/personal/otp/verify    body: { whatsapp_number, code }
13     POST /api/personal/email/request body: { manager_id }
14     GET  /api/personal/email/verify?token=... -> redirects to /personal/welcome
15   ============================================================ */
16
17(function () {
18    'use strict';
19
20    var API_BASE = (function () {
21        // Allow ?env=dev to flip to localhost for dev testing.
22        var u = new URL(window.location.href);
23        if (u.searchParams.get('env') === 'dev') return 'http://localhost:3000';
24        return 'https://api.orbis.restaurant';
25    })();
26
27    var STORAGE_KEY = 'orbis_personal_signup_v1';
28    var SCREEN_ORDER = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'];
29
30    var STATE = {
31        currentScreen: '1',
32        data: {
33            full_name: '',
34            whatsapp_number: '',
35            email: '',
36            date_of_birth: '',
37            role: '',
38            venue: '',
39            neighborhood: '',
40            shift_pattern: '',
41            responsibilities: '',
42            beverage_focus: '',
43            career_target: '',
44            mentors: ['', '', ''],
45            regulars: ['', '', ''],
46            brief_hour: 7,
47            tracking_focus: ''
48        },
49        manager_id: null,
50        whatsapp_verified: false,
51        email_verified: false
52    };
53
54    /* ============== persistence ============== */
55
56    function saveState() {
57        try {
58            localStorage.setItem(STORAGE_KEY, JSON.stringify({
59                currentScreen: STATE.currentScreen,
60                data: STATE.data,
61                manager_id: STATE.manager_id
62            }));
63        } catch (e) { /* ignore quota errors */ }
64    }
65    function loadState() {
66        try {
67            var raw = localStorage.getItem(STORAGE_KEY);
68            if (!raw) return null;
69            return JSON.parse(raw);
70        } catch (e) { return null; }
71    }
72    function clearState() {
73        try { localStorage.removeItem(STORAGE_KEY); } catch (e) {}
74    }
75
76    /* ============== validation ============== */
77
78    var REQUIRED_BY_SCREEN = {
79        '1': ['full_name', 'whatsapp_number', 'email'],
80        '2': ['role'],
81        '3': ['venue', 'neighborhood'],
82        '4': ['shift_pattern'],
83        '5': ['responsibilities'],
84        '6': [],                       // beverage_focus optional
85        '7': ['career_target'],
86        '8': ['mentors_0'],            // at least one mentor
87        '9': ['regulars_0'],           // at least one regular
88        '10': ['brief_hour'],
89        '11': []                       // tracking_focus optional
90    };
91
92    var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
93    // E.164: +<country><number>, total 7-15 digits after +
94    var PHONE_E164_RE = /^\+[1-9]\d{6,14}$/;
95
96    function normalizePhone(raw) {
97        // Require explicit country-code prefix. ORBIS Personal pilots live in
98        // multiple countries (e.g. Tbilisi nephew on +995); auto-prefixing +1
99        // silently corrupts non-US numbers. If the user did not include a +,
100        // we return '' so isValidPhone fails and the field-error guides them.
101        var s = String(raw || '').trim();
102        if (!s) return '';
103        if (s.charAt(0) === '+') {
104            return '+' + s.slice(1).replace(/\D/g, '');
105        }
106        return '';
107    }
108    function isValidPhone(raw) {
109        return PHONE_E164_RE.test(normalizePhone(raw));
110    }
111    function isValidEmail(s) { return EMAIL_RE.test(String(s || '').trim()); }
112
113    function fieldValue(name) {
114        if (name.indexOf('mentors_') === 0) return STATE.data.mentors[parseInt(name.slice(8), 10)] || '';
115        if (name.indexOf('regulars_') === 0) return STATE.data.regulars[parseInt(name.slice(9), 10)] || '';
116        return STATE.data[name];
117    }
118
119    function isScreenValid(screenId) {
120        var required = REQUIRED_BY_SCREEN[screenId] || [];
121        for (var i = 0; i < required.length; i++) {
122            var name = required[i];
123            var v = fieldValue(name);
124            if (!v || (typeof v === 'string' && v.trim().length < 1)) return false;
125            if (name === 'full_name' && v.trim().length < 2) return false;
126            if (name === 'whatsapp_number' && !isValidPhone(v)) return false;
127            if (name === 'email' && !isValidEmail(v)) return false;
128        }
129        return true;
130    }
131
132    /* ============== inline echo ============== */
133
134    var ECHO_BY_SCREEN = {
135        '2': function (v) {
136            var map = {
137                sommelier: "Got it. We'll calibrate the brief to wine-program reality.",
138                bartender: "Got it. Bar-program signals get priority.",
139                gm: "Got it. Operator-level signals lead the brief.",
140                foh_manager: "Got it. Floor-management signals lead.",
141                boh_lead: "Got it. Kitchen-side signals are surfaced first.",
142                server: "Got it. Brief stays grounded in service-level reality.",
143                host: "Got it. Front-door signals shape the brief.",
144                other: "Got it. Tell ORBIS more in your first reply."
145            };
146            return map[v] || '';
147        },
148        '3': function () { return "Brief will know your turf."; },
149        '4': function (v) {
150            var map = {
151                lunch: "Brief lands before lunch service.",
152                dinner: "Brief lands mid-afternoon, before dinner pre-shift.",
153                brunch_dinner: "Brief lands mid-morning, before brunch.",
154                late_night: "Brief lands late afternoon, on your clock.",
155                events: "Brief lands the morning of each event."
156            };
157            return map[v] || '';
158        },
159        '5': function () { return "Read. ORBIS works around what is actually on you."; },
160        '6': function (v) { return v && v.trim() ? "Noted. " + v.trim().split(/\s+/)[0] + " talk goes deeper now." : ''; },
161        '7': function () { return "Career radar tuned. Watching for openings."; },
162        '8': function () {
163            var names = STATE.data.mentors.filter(function (s) { return s && s.trim(); });
164            if (!names.length) return '';
165            return "We'll watch for openings from " + (names.length === 1 ? "this name." : "these names.");
166        },
167        '9': function () {
168            var names = STATE.data.regulars.filter(function (s) { return s && s.trim(); });
169            if (!names.length) return '';
170            return "Saved. ORBIS reminds you when they go quiet.";
171        },
172        '10': function (v) {
173            var n = parseInt(v, 10);
174            if (!n) return '';
175            return "Brief lands at " + n + " AM, your local time.";
176        },
177        '11': function (v) { return v && v.trim() ? "Set. First check tomorrow morning." : ''; }
178    };
179
180    function updateEcho(screenId) {
181        var screen = document.querySelector('.screen[data-screen="' + screenId + '"]');
182        if (!screen) return;
183        var echoEl = screen.querySelector('[data-echo]');
184        if (!echoEl) return;
185        var fn = ECHO_BY_SCREEN[screenId];
186        if (!fn) return;
187        var v;
188        if (screenId === '8' || screenId === '9') v = null;
189        else if (screenId === '3') v = STATE.data.neighborhood;
190        else {
191            var firstField = (REQUIRED_BY_SCREEN[screenId] || [])[0] || screen.querySelector('.field-input, .field-select, .field-textarea');
192            if (firstField && firstField.id) v = STATE.data[firstField.id];
193            else if (typeof firstField === 'string') v = STATE.data[firstField];
194        }
195        var text = fn(v);
196        if (text) {
197            echoEl.textContent = text;
198            requestAnimationFrame(function () { echoEl.classList.add('visible'); });
199        } else {
200            echoEl.classList.remove('visible');
201        }
202    }
203
204    /* ============== screen navigation ============== */
205
206    function showScreen(id) {
207        document.querySelectorAll('.screen').forEach(function (s) { s.classList.remove('active'); });
208        var next = document.querySelector('.screen[data-screen="' + id + '"]');
209        if (next) {
210            next.classList.add('active');
211            window.scrollTo({ top: 0, behavior: 'instant' });
212        }
213        STATE.currentScreen = id;
214        updateProgressUi(id);
215        updateBackBtn(id);
216        // Refresh CTA enabled-state for the screen (in case loaded from localStorage)
217        refreshCtaState(id);
218        // Echo if any answer was already saved
219        if (ECHO_BY_SCREEN[id]) updateEcho(id);
220        // Focus first input on screens 2-11
221        var idx = SCREEN_ORDER.indexOf(id);
222        if (idx >= 1 && idx <= 10) {
223            var firstInput = next && next.querySelector('input, select, textarea');
224            if (firstInput) setTimeout(function () { firstInput.focus(); }, 380);
225        }
226    }
227
228    function updateProgressUi(id) {
229        var wrap = document.getElementById('progressWrap');
230        var fill = document.getElementById('progressFill');
231        var label = document.getElementById('progressLabel');
232        var idx = SCREEN_ORDER.indexOf(id);
233        if (idx === -1 || id === '1') {
234            wrap.hidden = true;
235            return;
236        }
237        wrap.hidden = false;
238        var qNum = idx;        // screen 2 -> question 1, screen 11 -> question 10
239        var pct = (qNum / 10) * 100;
240        fill.style.width = pct + '%';
241        label.textContent = 'Question ' + qNum + ' of 10';
242    }
243
244    function updateBackBtn(id) {
245        var btn = document.getElementById('backBtn');
246        var idx = SCREEN_ORDER.indexOf(id);
247        if (idx > 0 && idx <= 10) {
248            btn.hidden = false;
249        } else {
250            btn.hidden = true;
251        }
252    }
253
254    function goBack() {
255        var idx = SCREEN_ORDER.indexOf(STATE.currentScreen);
256        if (idx > 0) showScreen(SCREEN_ORDER[idx - 1]);
257    }
258
259    function goNext() {
260        // PO.4 (2026-05-12): ALL Personal signups are short-path. After the
261        // user fills the four identity fields on screen 1 (full_name, whatsapp,
262        // email, date_of_birth) and taps Continue, we submit immediately and
263        // redirect to /personal/welcome - skipping wizard screens 2-11 plus
264        // the /verify (OTP) and /email-verify screens.
265        //
266        // The remaining anketa happens via WhatsApp: standard Level 1
267        // (steps 0-4) handler in English, then Level 2 (steps 10-17) for
268        // deeper profile - the same path for everyone (2026-08-24: the old
269        // surname-matched family-georgia cohort was retired on the backend,
270        // orbis-universal commit 666f456).
271        //
272        // This was the original product intent: one form, four fields, Open
273        // WhatsApp button. The OTP+email gate and the wizard screens 2-11
274        // were friction we added and never needed.
275        if (STATE.currentScreen === '1') {
276            saveState();
277            doSubmit();
278            return;
279        }
280        var idx = SCREEN_ORDER.indexOf(STATE.currentScreen);
281        if (idx >= 0 && idx < SCREEN_ORDER.length - 1) {
282            saveState();
283            showScreen(SCREEN_ORDER[idx + 1]);
284        } else if (STATE.currentScreen === '11') {
285            doSubmit();
286        }
287    }
288
289    /* ============== CTA state refresh ============== */
290
291    function refreshCtaState(screenId) {
292        var screen = document.querySelector('.screen[data-screen="' + screenId + '"]');
293        if (!screen) return;
294        var btn = screen.querySelector('.cta[data-action="next"], .cta[data-action="submit"]');
295        if (!btn) return;
296        btn.disabled = !isScreenValid(screenId);
297    }
298
299    /* ============== input wiring ============== */
300
301    function bindInputs() {
302        // Screen 1
303        bind('full_name', function (v) { STATE.data.full_name = v; });
304        bindPhone();
305        bind('email', function (v) { STATE.data.email = v.trim(); });
306        bind('date_of_birth', function (v) { STATE.data.date_of_birth = v.trim(); });
307
308        // Anketa screens
309        bind('role', function (v) { STATE.data.role = v; updateEcho('2'); });
310        bind('venue', function (v) { STATE.data.venue = v; });
311        bind('neighborhood', function (v) { STATE.data.neighborhood = v; updateEcho('3'); });
312        bind('shift_pattern', function (v) { STATE.data.shift_pattern = v; updateEcho('4'); });
313        bind('responsibilities', function (v) { STATE.data.responsibilities = v; updateEcho('5'); });
314        bind('beverage_focus', function (v) { STATE.data.beverage_focus = v; updateEcho('6'); });
315        bind('career_target', function (v) { STATE.data.career_target = v; updateEcho('7'); });
316
317        for (var i = 0; i < 3; i++) {
318            (function (idx) {
319                bind('mentors_' + idx, function (v) {
320                    STATE.data.mentors[idx] = v;
321                    updateEcho('8');
322                    refreshCtaState('8');
323                });
324                bind('regulars_' + idx, function (v) {
325                    STATE.data.regulars[idx] = v;
326                    updateEcho('9');
327                    refreshCtaState('9');
328                });
329            })(i);
330        }
331        bind('brief_hour', function (v) { STATE.data.brief_hour = parseInt(v, 10) || 7; updateEcho('10'); });
332        bind('tracking_focus', function (v) { STATE.data.tracking_focus = v; updateEcho('11'); });
333
334        // Counters
335        document.querySelectorAll('[data-counter-for]').forEach(function (el) {
336            var id = el.getAttribute('data-counter-for');
337            var input = document.getElementById(id);
338            if (!input) return;
339            input.addEventListener('input', function () {
340                el.textContent = (input.value || '').length;
341            });
342        });
343    }
344
345    function bind(id, setter) {
346        var el = document.getElementById(id);
347        if (!el) return;
348        var screen = el.closest('.screen');
349        var screenId = screen ? screen.getAttribute('data-screen') : null;
350        var fire = function () {
351            setter(el.value);
352            saveState();
353            if (screenId) refreshCtaState(screenId);
354        };
355        el.addEventListener('input', fire);
356        el.addEventListener('change', fire);
357        el.addEventListener('blur', fire);
358    }
359
360    function bindPhone() {
361        var el = document.getElementById('whatsapp_number');
362        if (!el) return;
363        var screenId = '1';
364        var fire = function () {
365            STATE.data.whatsapp_number = normalizePhone(el.value);
366            saveState();
367            refreshCtaState(screenId);
368        };
369        el.addEventListener('input', fire);
370        el.addEventListener('blur', fire);
371    }
372
373    /* ============== prefill from saved state ============== */
374
375    function prefillFromState() {
376        // identity
377        setVal('full_name', STATE.data.full_name);
378        setVal('email', STATE.data.email);
379        setVal('date_of_birth', STATE.data.date_of_birth);
380        // Country code is now part of the input itself; no separate +1 badge.
381        setVal('whatsapp_number', STATE.data.whatsapp_number || '');
382        // anketa
383        ['role','venue','neighborhood','shift_pattern','responsibilities','beverage_focus','career_target','tracking_focus']
384            .forEach(function (k) { setVal(k, STATE.data[k]); });
385        for (var i = 0; i < 3; i++) {
386            setVal('mentors_' + i, STATE.data.mentors[i]);
387            setVal('regulars_' + i, STATE.data.regulars[i]);
388        }
389        setVal('brief_hour', String(STATE.data.brief_hour || 7));
390        // counters
391        document.querySelectorAll('[data-counter-for]').forEach(function (el) {
392            var id = el.getAttribute('data-counter-for');
393            var input = document.getElementById(id);
394            if (input) el.textContent = (input.value || '').length;
395        });
396    }
397    function setVal(id, v) {
398        var el = document.getElementById(id);
399        if (el && v != null) el.value = v;
400    }
401
402    /* ============== submit + verify flow ============== */
403
404    function doSubmit() {
405        showScreen('reading');
406        // Brief perceived-intelligence pause
407        setTimeout(function () { postSignup(); }, 1500);
408    }
409
410    function postSignup() {
411        var payload = buildPayload();
412        fetch(API_BASE + '/api/personal/signup', {
413            method: 'POST',
414            headers: { 'Content-Type': 'application/json' },
415            body: JSON.stringify(payload)
416        }).then(function (r) { return r.json(); }).then(function (j) {
417            if (!j || j.status !== 'ok') {
418                alert('Something went wrong. Please try again.');
419                showScreen('11');
420                return;
421            }
422            STATE.manager_id = j.manager_id;
423            STATE.whatsapp_verified = !!j.whatsapp_verified;
424            STATE.email_verified = !!j.email_verified;
425            saveState();
426
427            if (j.already_existed && j.whatsapp_verified && j.email_verified) {
428                clearState();
429                window.location.href = '/personal/welcome/';
430                return;
431            }
432            if (!j.whatsapp_verified) {
433                proceedToOtp();
434            } else if (!j.email_verified) {
435                proceedToEmail();
436            } else {
437                clearState();
438                window.location.href = '/personal/welcome/';
439            }
440        }).catch(function () {
441            alert('Network hiccup. Please try again.');
442            showScreen('11');
443        });
444    }
445
446    function buildPayload() {
447        var d = STATE.data;
448        // PO.4 (2026-05-12): identity-only payload for ALL Personal signups.
449        // The remaining anketa happens through WhatsApp (Level 1 for non-FG,
450        // FG state machine for surname-matched family pilots) so the form
451        // does not need role/venue/neighborhood/etc.
452        return {
453            full_name: d.full_name.trim(),
454            whatsapp_number: normalizePhone(d.whatsapp_number),
455            email: d.email.trim(),
456            date_of_birth: (d.date_of_birth || '').trim() || null
457        };
458    }
459
460    function maskPhone(p) {
461        if (!p) return '';
462        // E.164 input has variable country-code length (+1, +44, +995, ...);
463        // mask middle digits but keep the +cc prefix and the last 4 digits.
464        var s = String(p);
465        if (s.length < 6) return s;
466        var last = s.slice(-4);
467        // Country code is everything between leading + and the first 9-10 digits
468        // of the national number; pull off "+" plus 1-3 digits for the head.
469        var m = s.match(/^(\+\d{1,3})/);
470        var head = m ? m[1] : s.slice(0, 2);
471        return head + ' ••• ••• ' + last;
472    }
473
474    /* ============== OTP screen ============== */
475
476    function proceedToOtp() {
477        document.getElementById('otpMaskedNumber').textContent = maskPhone(STATE.data.whatsapp_number);
478        showScreen('otp');
479        requestOtp();
480        wireOtpBoxes();
481    }
482
483    function requestOtp() {
484        return fetch(API_BASE + '/api/personal/otp/request', {
485            method: 'POST',
486            headers: { 'Content-Type': 'application/json' },
487            body: JSON.stringify({ whatsapp_number: STATE.data.whatsapp_number })
488        }).catch(function () { /* swallow; user can resend */ });
489    }
490
491    var otpResendCooldown = 0;
492    function wireOtpResend() {
493        var btn = document.getElementById('otpResend');
494        if (!btn || btn._wired) return;
495        btn._wired = true;
496        btn.addEventListener('click', function () {
497            if (otpResendCooldown > 0) return;
498            requestOtp();
499            otpResendCooldown = 30;
500            tickResendCooldown(btn, function () {
501                otpResendCooldown--;
502                return otpResendCooldown > 0 ? 'Resend in ' + otpResendCooldown + 's' : 'Resend code';
503            });
504        });
505    }
506
507    function wireEmailResend() {
508        var btn = document.getElementById('emailResend');
509        if (!btn || btn._wired) return;
510        btn._wired = true;
511        btn.addEventListener('click', function () {
512            if (otpResendCooldown > 0) return;
513            requestEmail();
514            otpResendCooldown = 30;
515            tickResendCooldown(btn, function () {
516                otpResendCooldown--;
517                return otpResendCooldown > 0 ? 'Resend in ' + otpResendCooldown + 's' : 'Resend email';
518            });
519        });
520    }
521
522    function tickResendCooldown(btn, computeLabel) {
523        btn.disabled = true;
524        var iv = setInterval(function () {
525            var label = computeLabel();
526            btn.textContent = label;
527            if (otpResendCooldown <= 0) {
528                clearInterval(iv);
529                btn.disabled = false;
530            }
531        }, 1000);
532    }
533
534    function wireOtpBoxes() {
535        var boxes = document.querySelectorAll('#otpRow .otp-box');
536        if (!boxes.length || boxes[0]._wired) return;
537        wireOtpResend();
538        boxes.forEach(function (box, i) {
539            box._wired = true;
540            box.addEventListener('input', function (e) {
541                var v = box.value.replace(/\D/g, '').slice(-1);
542                box.value = v;
543                if (v && i < boxes.length - 1) boxes[i + 1].focus();
544                if (collectOtp().length === 6) submitOtp();
545            });
546            box.addEventListener('keydown', function (e) {
547                if (e.key === 'Backspace' && !box.value && i > 0) {
548                    boxes[i - 1].focus();
549                    boxes[i - 1].value = '';
550                }
551                if (e.key === 'ArrowLeft' && i > 0) boxes[i - 1].focus();
552                if (e.key === 'ArrowRight' && i < boxes.length - 1) boxes[i + 1].focus();
553            });
554            box.addEventListener('paste', function (e) {
555                e.preventDefault();
556                var t = (e.clipboardData || window.clipboardData).getData('text');
557                var digits = String(t || '').replace(/\D/g, '').slice(0, 6);
558                if (!digits) return;
559                for (var k = 0; k < digits.length && k < boxes.length; k++) boxes[k].value = digits.charAt(k);
560                var next = Math.min(digits.length, boxes.length - 1);
561                boxes[next].focus();
562                if (collectOtp().length === 6) submitOtp();
563            });
564        });
565        setTimeout(function () { boxes[0].focus(); }, 380);
566    }
567
568    function collectOtp() {
569        var s = '';
570        document.querySelectorAll('#otpRow .otp-box').forEach(function (b) { s += b.value || ''; });
571        return s;
572    }
573
574    function submitOtp() {
575        var code = collectOtp();
576        if (code.length !== 6) return;
577        var boxes = document.querySelectorAll('#otpRow .otp-box');
578        boxes.forEach(function (b) { b.classList.remove('error'); });
579        fetch(API_BASE + '/api/personal/otp/verify', {
580            method: 'POST',
581            headers: { 'Content-Type': 'application/json' },
582            body: JSON.stringify({ whatsapp_number: STATE.data.whatsapp_number, code: code })
583        }).then(function (r) { return r.json(); }).then(function (j) {
584            if (j && j.status === 'ok') {
585                STATE.whatsapp_verified = true;
586                saveState();
587                if (STATE.email_verified) {
588                    clearState();
589                    window.location.href = '/personal/welcome/';
590                } else {
591                    proceedToEmail();
592                }
593            } else {
594                flashOtpError();
595            }
596        }).catch(function () { flashOtpError(); });
597    }
598
599    function flashOtpError() {
600        var boxes = document.querySelectorAll('#otpRow .otp-box');
601        boxes.forEach(function (b) { b.classList.add('error'); });
602        setTimeout(function () {
603            boxes.forEach(function (b) { b.value = ''; b.classList.remove('error'); });
604            boxes[0].focus();
605        }, 600);
606    }
607
608    /* ============== Email screen + polling ============== */
609
610    var emailPollInterval = null;
611
612    function proceedToEmail() {
613        document.getElementById('emailDisplay').textContent = STATE.data.email;
614        showScreen('email');
615        requestEmail();
616        wireEmailResend();
617        startEmailPolling();
618    }
619
620    function requestEmail() {
621        if (!STATE.manager_id) return Promise.resolve();
622        return fetch(API_BASE + '/api/personal/email/request', {
623            method: 'POST',
624            headers: { 'Content-Type': 'application/json' },
625            body: JSON.stringify({ manager_id: STATE.manager_id })
626        }).catch(function () {});
627    }
628
629    function startEmailPolling() {
630        stopEmailPolling();
631        emailPollInterval = setInterval(checkEmailStatus, 4500);
632        document.addEventListener('visibilitychange', handleVisibility);
633    }
634    function stopEmailPolling() {
635        if (emailPollInterval) { clearInterval(emailPollInterval); emailPollInterval = null; }
636    }
637    function handleVisibility() {
638        if (document.hidden) {
639            stopEmailPolling();
640        } else if (STATE.currentScreen === 'email' && !STATE.email_verified) {
641            checkEmailStatus();        // immediate check on return
642            emailPollInterval = setInterval(checkEmailStatus, 4500);
643        }
644    }
645    function checkEmailStatus() {
646        if (!STATE.manager_id) return;
647        // Re-call the idempotent signup endpoint with the same payload as a 
647status check.
648        // Backend returns latest verification flags; this avoids needing a separate status endpoint.
649        fetch(API_BASE + '/api/personal/signup', {
650            method: 'POST',
651            headers: { 'Content-Type': 'application/json' },
652            body: JSON.stringify(buildPayload())
653        }).then(function (r) { return r.json(); }).then(function (j) {
654            if (j && j.email_verified) {
655                STATE.email_verified = true;
656                stopEmailPolling();
657                clearState();
658                window.location.href = '/personal/welcome/';
659            }
660        }).catch(function () {});
661    }
662
663    /* ============== global handlers ============== */
664
665    function bindGlobalActions() {
666        document.getElementById('backBtn').addEventListener('click', goBack);
667
668        document.querySelectorAll('[data-action="next"]').forEach(function (b) {
669            b.addEventListener('click', function () { if (!b.disabled) goNext(); });
670        });
671        document.querySelectorAll('[data-action="submit"]').forEach(function (b) {
672            b.addEventListener('click', function () {
673                if (b.disabled) return;
674                doSubmit();
675            });
676        });
677        document.querySelectorAll('[data-action="edit-number"]').forEach(function (b) {
678            b.addEventListener('click', function () {
679                stopEmailPolling();
680                showScreen('1');
681            });
682        });
683
684        // Recovery modal
685        document.getElementById('recoveryContinue').addEventListener('click', function () {
686            document.getElementById('recoveryOverlay').classList.remove('active');
687            showScreen(STATE.currentScreen);
688        });
689        document.getElementById('recoveryReset').addEventListener('click', function () {
690            clearState();
691            // hard-reload to reset to factory STATE
692            window.location.reload();
693        });
694    }
695
696    /* ============== bootstrap ============== */
697
698    function init() {
699        bindInputs();
700        bindGlobalActions();
701
702        var saved = loadState();
703        if (saved && saved.currentScreen && saved.currentScreen !== '1') {
704            // Hydrate STATE
705            STATE.currentScreen = saved.currentScreen;
706            if (saved.data) Object.keys(saved.data).forEach(function (k) {
707                if (k === 'mentors' || k === 'regulars') {
708                    STATE.data[k] = (saved.data[k] || []).slice(0, 3);
709                    while (STATE.data[k].length < 3) STATE.data[k].push('');
710                } else {
711                    STATE.data[k] = saved.data[k];
712                }
713            });
714            STATE.manager_id = saved.manager_id || null;
715            prefillFromState();
716            // Show recovery prompt
717            document.getElementById('recoveryOverlay').classList.add('active');
718        } else {
719            showScreen('1');
720        }
721    }
722
723    if (document.readyState === 'loading') {
724        document.addEventListener('DOMContentLoaded', init);
725    } else {
726        init();
727    }
728})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.