clay-server 2.8.2 → 2.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +2 -0
  2. package/bin/cli.js +122 -2
  3. package/lib/config.js +20 -1
  4. package/lib/daemon.js +40 -0
  5. package/lib/pages.js +671 -27
  6. package/lib/project.js +280 -42
  7. package/lib/public/app.js +321 -84
  8. package/lib/public/css/admin.css +576 -0
  9. package/lib/public/css/base.css +3 -0
  10. package/lib/public/css/filebrowser.css +8 -1
  11. package/lib/public/css/home-hub.css +1 -1
  12. package/lib/public/css/icon-strip.css +1 -0
  13. package/lib/public/css/input.css +4 -0
  14. package/lib/public/css/menus.css +16 -51
  15. package/lib/public/css/messages.css +1 -0
  16. package/lib/public/css/mobile-nav.css +2 -2
  17. package/lib/public/css/overlays.css +177 -20
  18. package/lib/public/css/scheduler.css +1 -0
  19. package/lib/public/css/server-settings.css +37 -34
  20. package/lib/public/css/sidebar.css +49 -0
  21. package/lib/public/css/title-bar.css +45 -8
  22. package/lib/public/index.html +96 -25
  23. package/lib/public/manifest.json +2 -2
  24. package/lib/public/modules/admin.js +631 -0
  25. package/lib/public/modules/markdown.js +9 -5
  26. package/lib/public/modules/notifications.js +15 -103
  27. package/lib/public/modules/profile.js +21 -0
  28. package/lib/public/modules/project-settings.js +4 -1
  29. package/lib/public/modules/scheduler.js +55 -48
  30. package/lib/public/modules/server-settings.js +26 -4
  31. package/lib/public/modules/sidebar.js +111 -5
  32. package/lib/public/style.css +1 -0
  33. package/lib/push.js +6 -0
  34. package/lib/server.js +1075 -27
  35. package/lib/sessions.js +127 -41
  36. package/lib/smtp.js +221 -0
  37. package/lib/users.js +459 -0
  38. package/package.json +2 -1
package/lib/pages.js CHANGED
@@ -4,39 +4,30 @@ function pinPageHtml() {
4
4
  '<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">' +
5
5
  '<meta name="apple-mobile-web-app-capable" content="yes">' +
6
6
  '<title>Clay</title>' +
7
- '<style>' +
8
- '*{margin:0;padding:0;box-sizing:border-box}' +
9
- 'body{background:#2F2E2B;color:#E8E5DE;font-family:system-ui,-apple-system,sans-serif;' +
10
- 'min-height:100dvh;display:flex;align-items:center;justify-content:center;padding:20px}' +
11
- '.c{max-width:320px;width:100%;text-align:center}' +
12
- 'h1{color:#DA7756;font-size:22px;margin-bottom:8px}' +
13
- '.sub{color:#908B81;font-size:14px;margin-bottom:32px}' +
14
- 'input{width:100%;background:#393733;border:1px solid #3E3C37;border-radius:12px;' +
15
- 'color:#E8E5DE;font-size:24px;letter-spacing:12px;text-align:center;padding:14px;' +
16
- 'outline:none;font-family:inherit;-webkit-text-security:disc}' +
17
- 'input:focus{border-color:#DA7756}' +
18
- 'input::placeholder{letter-spacing:0;font-size:15px;color:#6D6860}' +
19
- '.err{color:#E5534B;font-size:13px;margin-top:12px;min-height:1.3em}' +
20
- '</style></head><body><div class="c">' +
21
- '<h1>Clay</h1>' +
22
- '<div class="sub">Enter PIN to continue</div>' +
23
- '<input id="pin" type="tel" maxlength="6" placeholder="6-digit PIN" autocomplete="off" inputmode="numeric">' +
7
+ '<style>' + authPageStyles + '</style></head><body><div class="c">' +
8
+ '<h1>Welcome back</h1>' +
9
+ '<div class="sub">Enter your PIN to continue</div>' +
10
+ pinBoxesHtml +
24
11
  '<div class="err" id="err"></div>' +
25
12
  '<script>' +
26
- 'var inp=document.getElementById("pin"),err=document.getElementById("err");' +
27
- 'inp.focus();' +
28
- 'inp.addEventListener("input",function(){' +
29
- 'if(inp.value.length===6){' +
13
+ pinBoxScript +
14
+ 'var err=document.getElementById("err");' +
15
+ 'function submitPin(){' +
16
+ 'var pin=document.getElementById("pin").value;' +
17
+ 'var boxes=document.querySelectorAll(".pin-digit");' +
30
18
  'fetch("/auth",{method:"POST",headers:{"Content-Type":"application/json"},' +
31
- 'body:JSON.stringify({pin:inp.value})})' +
19
+ 'body:JSON.stringify({pin:pin})})' +
32
20
  '.then(function(r){return r.json()})' +
33
21
  '.then(function(d){' +
34
22
  'if(d.ok){location.reload();return}' +
35
- 'if(d.locked){inp.disabled=true;err.textContent="Too many attempts. Try again in "+Math.ceil(d.retryAfter/60)+" min";' +
36
- 'setTimeout(function(){inp.disabled=false;err.textContent="";inp.focus()},d.retryAfter*1000);return}' +
23
+ 'if(d.locked){for(var i=0;i<boxes.length;i++)boxes[i].disabled=true;' +
24
+ 'err.textContent="Too many attempts. Try again in "+Math.ceil(d.retryAfter/60)+" min";' +
25
+ 'setTimeout(function(){for(var i=0;i<boxes.length;i++)boxes[i].disabled=false;' +
26
+ 'err.textContent="";resetPinBoxes()},d.retryAfter*1000);return}' +
37
27
  'var msg="Wrong PIN";if(typeof d.attemptsLeft==="number"&&d.attemptsLeft<=3)msg+=" ("+d.attemptsLeft+" left)";' +
38
- 'err.textContent=msg;inp.value="";inp.focus()})' +
39
- '.catch(function(){err.textContent="Connection error"})}});' +
28
+ 'err.textContent=msg;resetPinBoxes()})' +
29
+ '.catch(function(){err.textContent="Connection error"})}' +
30
+ 'initPinBoxes("pin-boxes","pin",submitPin);' +
40
31
  '</script></div></body></html>';
41
32
  }
42
33
 
@@ -661,4 +652,657 @@ function escapeHtml(s) {
661
652
  return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
662
653
  }
663
654
 
664
- module.exports = { pinPageHtml, setupPageHtml };
655
+ // --- Build auth page CSS variables from ayu-light theme (same logic as theme.js computeVars) ---
656
+ var path = require("path");
657
+ var _authTheme = require(path.join(__dirname, "themes", "ayu-light.json"));
658
+
659
+ function _hexToRgb(hex) {
660
+ hex = hex.replace("#", "");
661
+ return {
662
+ r: parseInt(hex.substring(0, 2), 16),
663
+ g: parseInt(hex.substring(2, 4), 16),
664
+ b: parseInt(hex.substring(4, 6), 16)
665
+ };
666
+ }
667
+
668
+ function _hexToRgba(hex, alpha) {
669
+ var c = _hexToRgb(hex);
670
+ return "rgba(" + c.r + "," + c.g + "," + c.b + "," + alpha + ")";
671
+ }
672
+
673
+ function _mixColors(hex1, hex2, weight) {
674
+ var c1 = _hexToRgb(hex1), c2 = _hexToRgb(hex2);
675
+ var w = weight;
676
+ var r = Math.round(c1.r * w + c2.r * (1 - w));
677
+ var g = Math.round(c1.g * w + c2.g * (1 - w));
678
+ var b = Math.round(c1.b * w + c2.b * (1 - w));
679
+ return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
680
+ }
681
+
682
+ var _t = {};
683
+ var _keys = ["base00","base01","base02","base03","base04","base05","base06","base07",
684
+ "base08","base09","base0A","base0B","base0C","base0D","base0E","base0F"];
685
+ for (var _ki = 0; _ki < _keys.length; _ki++) {
686
+ _t[_keys[_ki]] = "#" + _authTheme[_keys[_ki]];
687
+ }
688
+
689
+ var _authVarsObj = {
690
+ "--bg": _t.base00,
691
+ "--bg-alt": _t.base01,
692
+ "--text": _t.base06,
693
+ "--text-muted": _t.base04,
694
+ "--text-dimmer": _t.base03,
695
+ "--accent": _t.base09,
696
+ "--accent-15": _hexToRgba(_t.base09, 0.15),
697
+ "--accent-20": _hexToRgba(_t.base09, 0.20),
698
+ "--border": _t.base02,
699
+ "--input-bg": _mixColors(_t.base01, _t.base02, 0.5),
700
+ "--error": _t.base08,
701
+ };
702
+
703
+ var _authVarsStr = ":root{";
704
+ var _avKeys = Object.keys(_authVarsObj);
705
+ for (var _vi = 0; _vi < _avKeys.length; _vi++) {
706
+ _authVarsStr += _avKeys[_vi] + ":" + _authVarsObj[_avKeys[_vi]] + ";";
707
+ }
708
+ _authVarsStr += "}";
709
+
710
+ // --- Shared CSS for auth pages (Clay Light theme via CSS variables) ---
711
+ var authPageStyles =
712
+ _authVarsStr +
713
+ // Reset & layout
714
+ '*{margin:0;padding:0;box-sizing:border-box}' +
715
+ 'body{background:var(--bg);color:var(--text);font-family:system-ui,-apple-system,sans-serif;' +
716
+ 'min-height:100dvh;display:flex;align-items:center;justify-content:center;padding:20px}' +
717
+ '.c{max-width:380px;width:100%;text-align:center}' +
718
+ 'h1{color:var(--text);font-size:24px;font-weight:700;margin-bottom:6px}' +
719
+ '.sub{color:var(--text-muted);font-size:14px;margin-bottom:28px}' +
720
+ '.field{margin-bottom:16px;text-align:left}' +
721
+ '.field label{display:block;font-size:12px;color:var(--text-muted);margin-bottom:6px;text-transform:uppercase;letter-spacing:0.5px}' +
722
+ 'input{width:100%;background:var(--input-bg);border:1px solid var(--border);border-radius:12px;' +
723
+ 'color:var(--text);font-size:16px;padding:12px 14px;outline:none;font-family:inherit}' +
724
+ 'input:focus{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-15)}' +
725
+ 'input::placeholder{font-size:14px;color:var(--text-dimmer)}' +
726
+ // PIN digit boxes
727
+ '.pin-wrap{display:flex;gap:8px;justify-content:center}' +
728
+ '.pin-digit{width:44px;height:56px;background:var(--input-bg);border:1.5px solid var(--border);border-radius:8px;' +
729
+ 'color:var(--text);font-family:"Courier New",Courier,"Roboto Mono",monospace;font-size:28px;font-weight:700;' +
730
+ 'text-align:center;line-height:56px;outline:none;caret-color:transparent;' +
731
+ 'transition:border-color 0.15s,box-shadow 0.15s}' +
732
+ '.pin-digit:focus{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-20)}' +
733
+ '.pin-digit.filled{color:var(--text)}' +
734
+ // Legacy single-input fallback
735
+ '.pin-input{font-size:24px;letter-spacing:12px;text-align:center;-webkit-text-security:disc;' +
736
+ 'font-family:"Courier New",Courier,"Roboto Mono",monospace}' +
737
+ '.btn{width:100%;background:var(--accent);color:#fff;border:none;border-radius:12px;' +
738
+ 'padding:14px;font-size:15px;font-weight:600;cursor:pointer;font-family:inherit;' +
739
+ 'transition:opacity 0.15s;margin-top:8px}' +
740
+ '.btn:hover{opacity:0.9}' +
741
+ '.btn:disabled{opacity:0.4;cursor:default}' +
742
+ '.err{color:var(--error);font-size:13px;margin-top:12px;min-height:1.3em}' +
743
+ '.info{color:var(--text-dimmer);font-size:12px;margin-top:16px}' +
744
+ // Step wizard
745
+ '.step{display:none}.step.active{display:block}' +
746
+ '.steps-bar{display:flex;gap:6px;justify-content:center;margin-bottom:28px}' +
747
+ '.steps-dot{width:8px;height:8px;border-radius:50%;background:var(--border);transition:background 0.2s}' +
748
+ '.steps-dot.done{background:var(--accent)}.steps-dot.current{background:var(--accent)}';
749
+
750
+ // --- Shared JS for PIN digit boxes ---
751
+ // initPinBoxes(containerId, hiddenInputId, onComplete) — wires up 6 individual digit inputs
752
+ var pinBoxScript =
753
+ 'function initPinBoxes(cId,hId,onComplete){' +
754
+ 'var wrap=document.getElementById(cId),hidden=document.getElementById(hId);' +
755
+ 'var boxes=wrap.querySelectorAll(".pin-digit");' +
756
+ 'var digits=["","","","","",""];' +
757
+ 'boxes[0].focus();' +
758
+ 'function setDigit(idx,v){digits[idx]=v;boxes[idx].value=v?"\\u2022":"";boxes[idx].classList.toggle("filled",v.length>0);syncHidden()}' +
759
+ 'for(var i=0;i<boxes.length;i++){(function(idx){' +
760
+ 'boxes[idx].addEventListener("input",function(e){' +
761
+ 'var raw=this.value.replace(/[^0-9]/g,"");' +
762
+ 'if(!raw){setDigit(idx,"");return}' +
763
+ 'var v=raw.charAt(raw.length-1);' +
764
+ 'setDigit(idx,v);' +
765
+ 'if(v&&idx<5)boxes[idx+1].focus();' +
766
+ 'if(hidden.value.length===6&&onComplete)onComplete()});' +
767
+ 'boxes[idx].addEventListener("keydown",function(e){' +
768
+ 'if(e.key==="Backspace"){if(!digits[idx]&&idx>0){setDigit(idx-1,"");boxes[idx-1].focus()}else{setDigit(idx,"")}syncHidden();return}' +
769
+ 'if(e.key==="ArrowLeft"&&idx>0)boxes[idx-1].focus();' +
770
+ 'if(e.key==="ArrowRight"&&idx<5)boxes[idx+1].focus();' +
771
+ 'if(e.key==="Enter"&&hidden.value.length===6&&onComplete){e.preventDefault();onComplete()}});' +
772
+ 'boxes[idx].addEventListener("paste",function(e){' +
773
+ 'e.preventDefault();var d=(e.clipboardData||window.clipboardData).getData("text").replace(/[^0-9]/g,"").slice(0,6);' +
774
+ 'for(var j=0;j<d.length&&j<6;j++){setDigit(j,d.charAt(j))}' +
775
+ 'if(d.length>=6){boxes[5].focus();if(onComplete)onComplete()}else if(d.length>0)boxes[d.length].focus()});' +
776
+ 'boxes[idx].addEventListener("focus",function(){this.select()});' +
777
+ '})(i)}' +
778
+ 'function syncHidden(){var v="";for(var j=0;j<digits.length;j++)v+=digits[j];hidden.value=v}' +
779
+ 'window.resetPinBoxes=function(){for(var j=0;j<6;j++){digits[j]="";boxes[j].value="";boxes[j].classList.remove("filled")}hidden.value="";boxes[0].focus()}' +
780
+ '}';
781
+
782
+ // HTML fragment for 6 PIN digit boxes + hidden input
783
+ var pinBoxesHtml =
784
+ '<div class="pin-wrap" id="pin-boxes">' +
785
+ '<input class="pin-digit" type="tel" maxlength="1" inputmode="numeric" autocomplete="off">' +
786
+ '<input class="pin-digit" type="tel" maxlength="1" inputmode="numeric" autocomplete="off">' +
787
+ '<input class="pin-digit" type="tel" maxlength="1" inputmode="numeric" autocomplete="off">' +
788
+ '<input class="pin-digit" type="tel" maxlength="1" inputmode="numeric" autocomplete="off">' +
789
+ '<input class="pin-digit" type="tel" maxlength="1" inputmode="numeric" autocomplete="off">' +
790
+ '<input class="pin-digit" type="tel" maxlength="1" inputmode="numeric" autocomplete="off">' +
791
+ '</div>' +
792
+ '<input type="hidden" id="pin">';
793
+
794
+ // --- Admin Setup Page (4-step wizard: setup code → email → display name → PIN) ---
795
+ function adminSetupPageHtml() {
796
+ return '<!DOCTYPE html><html lang="en"><head>' +
797
+ '<meta charset="UTF-8">' +
798
+ '<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">' +
799
+ '<title>Admin Setup - Clay</title>' +
800
+ '<style>' + authPageStyles + '</style></head><body><div class="c">' +
801
+ '<div class="steps-bar"><span class="steps-dot current" id="dot0"></span><span class="steps-dot" id="dot1"></span><span class="steps-dot" id="dot2"></span><span class="steps-dot" id="dot3"></span></div>' +
802
+
803
+ // Step 1: Setup Code
804
+ '<div class="step active" id="step0">' +
805
+ '<h1>Set up your server</h1>' +
806
+ '<div class="sub">Enter the 6-character code shown in your terminal</div>' +
807
+ '<div class="field"><label>Setup Code</label>' +
808
+ '<input id="code" type="text" maxlength="6" placeholder="6-character code" autocomplete="off" autofocus></div>' +
809
+ '<button class="btn" id="btn0" disabled>Continue</button>' +
810
+ '<div class="err" id="err0"></div>' +
811
+ '</div>' +
812
+
813
+ // Step 2: Username
814
+ '<div class="step" id="step1">' +
815
+ '<h1>Pick a username</h1>' +
816
+ '<div class="sub">This is how others will identify you</div>' +
817
+ '<div class="field"><label>Username</label>' +
818
+ '<input id="username" type="text" maxlength="100" placeholder="Username" autocomplete="username"></div>' +
819
+ '<button class="btn" id="btn1" disabled>Continue</button>' +
820
+ '<div class="err" id="err1"></div>' +
821
+ '</div>' +
822
+
823
+ // Step 3: Display Name
824
+ '<div class="step" id="step2">' +
825
+ '<h1>What should we call you?</h1>' +
826
+ '<div class="sub">Your display name is shown in conversations</div>' +
827
+ '<div class="field"><label>Display Name</label>' +
828
+ '<input id="displayname" type="text" maxlength="30" placeholder="Your name" autocomplete="name"></div>' +
829
+ '<button class="btn" id="btn2" disabled>Continue</button>' +
830
+ '<div class="err" id="err2"></div>' +
831
+ '</div>' +
832
+
833
+ // Step 4: PIN
834
+ '<div class="step" id="step3">' +
835
+ '<h1>Secure your account</h1>' +
836
+ '<div class="sub">Set a 6-digit PIN for quick login</div>' +
837
+ pinBoxesHtml +
838
+ '<button class="btn" id="btn3" disabled style="margin-top:20px">Create Account</button>' +
839
+ '<div class="err" id="err3"></div>' +
840
+ '</div>' +
841
+
842
+ '<script>' +
843
+ pinBoxScript +
844
+ 'var step=0;' +
845
+ 'var codeEl=document.getElementById("code"),usernameEl=document.getElementById("username"),dnEl=document.getElementById("displayname"),pinEl=document.getElementById("pin");' +
846
+ 'var steps=[document.getElementById("step0"),document.getElementById("step1"),document.getElementById("step2"),document.getElementById("step3")];' +
847
+ 'var dots=[document.getElementById("dot0"),document.getElementById("dot1"),document.getElementById("dot2"),document.getElementById("dot3")];' +
848
+ 'var btns=[document.getElementById("btn0"),document.getElementById("btn1"),document.getElementById("btn2"),document.getElementById("btn3")];' +
849
+ 'var errs=[document.getElementById("err0"),document.getElementById("err1"),document.getElementById("err2"),document.getElementById("err3")];' +
850
+
851
+ 'function goStep(n){' +
852
+ 'steps[step].classList.remove("active");dots[step].classList.remove("current");dots[step].classList.add("done");' +
853
+ 'step=n;steps[step].classList.add("active");dots[step].classList.add("current");' +
854
+ 'errs[step].textContent="";' +
855
+ 'if(step===1)usernameEl.focus();' +
856
+ 'if(step===2){dnEl.focus();if(!dnEl.value)dnEl.value=usernameEl.value}' +
857
+ 'if(step===3){initPinBoxes("pin-boxes","pin",function(){if(!btns[3].disabled)doSetup()});' +
858
+ 'var boxes=document.querySelectorAll(".pin-digit");' +
859
+ 'for(var i=0;i<boxes.length;i++)boxes[i].addEventListener("input",function(){btns[3].disabled=pinEl.value.length!==6})}' +
860
+ '}' +
861
+
862
+ // Step 1 validation
863
+ 'codeEl.addEventListener("input",function(){btns[0].disabled=codeEl.value.length<4});' +
864
+ 'btns[0].onclick=function(){goStep(1)};' +
865
+
866
+ // Step 2 validation (username)
867
+ 'usernameEl.addEventListener("input",function(){btns[1].disabled=usernameEl.value.trim().length<1});' +
868
+ 'usernameEl.addEventListener("keydown",function(e){if(e.key==="Enter"&&!btns[1].disabled)goStep(2)});' +
869
+ 'btns[1].onclick=function(){goStep(2)};' +
870
+
871
+ // Step 3 validation (display name)
872
+ 'dnEl.addEventListener("input",function(){btns[2].disabled=dnEl.value.trim().length<1});' +
873
+ 'dnEl.addEventListener("keydown",function(e){if(e.key==="Enter"&&!btns[2].disabled)goStep(3)});' +
874
+ 'btns[2].onclick=function(){goStep(3)};' +
875
+
876
+ 'function doSetup(){' +
877
+ 'btns[3].disabled=true;errs[3].textContent="";' +
878
+ 'fetch("/auth/setup",{method:"POST",headers:{"Content-Type":"application/json"},' +
879
+ 'body:JSON.stringify({setupCode:codeEl.value,username:usernameEl.value.trim(),displayName:dnEl.value.trim(),pin:pinEl.value})})' +
880
+ '.then(function(r){return r.json()})' +
881
+ '.then(function(d){' +
882
+ 'if(d.ok){location.href="/";return}' +
883
+ 'errs[3].textContent=d.error||"Setup failed";btns[3].disabled=false})' +
884
+ '.catch(function(){errs[3].textContent="Connection error";btns[3].disabled=false})}' +
885
+ 'btns[3].onclick=doSetup;' +
886
+ '</script></div></body></html>';
887
+ }
888
+
889
+ // --- Multi-user Login Page ---
890
+ function multiUserLoginPageHtml() {
891
+ return '<!DOCTYPE html><html lang="en"><head>' +
892
+ '<meta charset="UTF-8">' +
893
+ '<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">' +
894
+ '<title>Login - Clay</title>' +
895
+ '<style>' + authPageStyles + '</style></head><body><div class="c">' +
896
+ '<div class="steps-bar"><span class="steps-dot current" id="dot0"></span><span class="steps-dot" id="dot1"></span></div>' +
897
+
898
+ // Step 1: Username
899
+ '<div class="step active" id="step0">' +
900
+ '<h1>Welcome back</h1>' +
901
+ '<div class="sub">Enter your username to log in</div>' +
902
+ '<div class="field"><label>Username</label>' +
903
+ '<input id="username" type="text" maxlength="100" placeholder="Username" autocomplete="username" autofocus></div>' +
904
+ '<button class="btn" id="btn0" disabled>Continue</button>' +
905
+ '<div class="err" id="err0"></div>' +
906
+ '</div>' +
907
+
908
+ // Step 2: PIN
909
+ '<div class="step" id="step1">' +
910
+ '<h1>Enter your PIN</h1>' +
911
+ '<div class="sub">6-digit PIN for your account</div>' +
912
+ pinBoxesHtml +
913
+ '<button class="btn" id="btn1" disabled style="margin-top:20px">Log In</button>' +
914
+ '<div class="err" id="err1"></div>' +
915
+ '</div>' +
916
+
917
+ '<script>' +
918
+ pinBoxScript +
919
+ 'var step=0;' +
920
+ 'var usernameEl=document.getElementById("username"),pinEl=document.getElementById("pin");' +
921
+ 'var steps=[document.getElementById("step0"),document.getElementById("step1")];' +
922
+ 'var dots=[document.getElementById("dot0"),document.getElementById("dot1")];' +
923
+ 'var btns=[document.getElementById("btn0"),document.getElementById("btn1")];' +
924
+ 'var errs=[document.getElementById("err0"),document.getElementById("err1")];' +
925
+
926
+ 'function goStep(n){' +
927
+ 'steps[step].classList.remove("active");dots[step].classList.remove("current");dots[step].classList.add("done");' +
928
+ 'step=n;steps[step].classList.add("active");dots[step].classList.add("current");' +
929
+ 'errs[step].textContent="";' +
930
+ 'if(step===1){initPinBoxes("pin-boxes","pin",function(){if(!btns[1].disabled)doLogin()});' +
931
+ 'var boxes=document.querySelectorAll(".pin-digit");' +
932
+ 'for(var i=0;i<boxes.length;i++)boxes[i].addEventListener("input",function(){btns[1].disabled=pinEl.value.length!==6})}' +
933
+ '}' +
934
+
935
+ // Step 1: username validation
936
+ 'usernameEl.addEventListener("input",function(){btns[0].disabled=usernameEl.value.length<1});' +
937
+ 'usernameEl.addEventListener("keydown",function(e){if(e.key==="Enter"&&!btns[0].disabled)goStep(1)});' +
938
+ 'btns[0].onclick=function(){goStep(1)};' +
939
+
940
+ 'function resetPin(){' +
941
+ 'var boxes=document.querySelectorAll(".pin-digit");' +
942
+ 'for(var i=0;i<boxes.length;i++)boxes[i].disabled=false;' +
943
+ 'if(window.resetPinBoxes)resetPinBoxes();btns[1].disabled=true}' +
944
+
945
+ 'function goBackToUsername(){' +
946
+ 'steps[1].classList.remove("active");dots[1].classList.remove("current");dots[1].classList.remove("done");' +
947
+ 'dots[0].classList.remove("done");dots[0].classList.add("current");' +
948
+ 'steps[0].classList.add("active");step=0;' +
949
+ 'errs[0].textContent="";errs[1].textContent="";usernameEl.focus()}' +
950
+
951
+ 'function doLogin(){' +
952
+ 'btns[1].disabled=true;errs[1].textContent="";' +
953
+ 'fetch("/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},' +
954
+ 'body:JSON.stringify({username:usernameEl.value,pin:pinEl.value})})' +
955
+ '.then(function(r){return r.json()})' +
956
+ '.then(function(d){' +
957
+ 'if(d.ok){location.reload();return}' +
958
+ 'if(d.locked){var boxes=document.querySelectorAll(".pin-digit");' +
959
+ 'for(var i=0;i<boxes.length;i++)boxes[i].disabled=true;' +
960
+ 'errs[1].textContent="Too many attempts. Try again in "+Math.ceil(d.retryAfter/60)+" min";' +
961
+ 'setTimeout(function(){resetPin()},d.retryAfter*1000);return}' +
962
+ 'var msg=d.error||"Invalid credentials";' +
963
+ 'if(typeof d.attemptsLeft==="number"&&d.attemptsLeft<=3)msg+=" ("+d.attemptsLeft+" left)";' +
964
+ 'errs[1].textContent=msg;resetPin()})' +
965
+ '.catch(function(){errs[1].textContent="Connection error";btns[1].disabled=false})}' +
966
+ 'btns[1].onclick=doLogin;' +
967
+ '</script></div></body></html>';
968
+ }
969
+
970
+ // --- Invite Registration Page (3-step wizard: email → display name → PIN) ---
971
+ function invitePageHtml(inviteCode) {
972
+ return '<!DOCTYPE html><html lang="en"><head>' +
973
+ '<meta charset="UTF-8">' +
974
+ '<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">' +
975
+ '<title>Join - Clay</title>' +
976
+ '<style>' + authPageStyles + '</style></head><body><div class="c">' +
977
+ '<div class="steps-bar"><span class="steps-dot current" id="dot0"></span><span class="steps-dot" id="dot1"></span><span class="steps-dot" id="dot2"></span></div>' +
978
+
979
+ // Step 1: Username
980
+ '<div class="step active" id="step0">' +
981
+ '<h1>You&#39;re invited!</h1>' +
982
+ '<div class="sub">Pick a username to get started</div>' +
983
+ '<div class="field"><label>Username</label>' +
984
+ '<input id="username" type="text" maxlength="100" placeholder="Username" autocomplete="username" autofocus></div>' +
985
+ '<button class="btn" id="btn0" disabled>Continue</button>' +
986
+ '<div class="err" id="err0"></div>' +
987
+ '</div>' +
988
+
989
+ // Step 2: Display Name
990
+ '<div class="step" id="step1">' +
991
+ '<h1>What should we call you?</h1>' +
992
+ '<div class="sub">Your display name is shown in conversations</div>' +
993
+ '<div class="field"><label>Display Name</label>' +
994
+ '<input id="displayname" type="text" maxlength="30" placeholder="Your name" autocomplete="name"></div>' +
995
+ '<button class="btn" id="btn1" disabled>Continue</button>' +
996
+ '<div class="err" id="err1"></div>' +
997
+ '</div>' +
998
+
999
+ // Step 3: PIN
1000
+ '<div class="step" id="step2">' +
1001
+ '<h1>Secure your account</h1>' +
1002
+ '<div class="sub">Set a 6-digit PIN for quick login</div>' +
1003
+ pinBoxesHtml +
1004
+ '<button class="btn" id="btn2" disabled style="margin-top:20px">Create Account</button>' +
1005
+ '<div class="err" id="err2"></div>' +
1006
+ '</div>' +
1007
+
1008
+ '<script>' +
1009
+ pinBoxScript +
1010
+ 'var inviteCode=' + JSON.stringify(inviteCode) + ';' +
1011
+ 'var step=0;' +
1012
+ 'var usernameEl=document.getElementById("username"),dnEl=document.getElementById("displayname"),pinEl=document.getElementById("pin");' +
1013
+ 'var steps=[document.getElementById("step0"),document.getElementById("step1"),document.getElementById("step2")];' +
1014
+ 'var dots=[document.getElementById("dot0"),document.getElementById("dot1"),document.getElementById("dot2")];' +
1015
+ 'var btns=[document.getElementById("btn0"),document.getElementById("btn1"),document.getElementById("btn2")];' +
1016
+ 'var errs=[document.getElementById("err0"),document.getElementById("err1"),document.getElementById("err2")];' +
1017
+
1018
+ 'function goStep(n){' +
1019
+ 'steps[step].classList.remove("active");dots[step].classList.remove("current");dots[step].classList.add("done");' +
1020
+ 'step=n;steps[step].classList.add("active");dots[step].classList.add("current");' +
1021
+ 'errs[step].textContent="";' +
1022
+ 'if(step===1){dnEl.focus();if(!dnEl.value)dnEl.value=usernameEl.value}' +
1023
+ 'if(step===2){initPinBoxes("pin-boxes","pin",function(){if(!btns[2].disabled)doRegister()});' +
1024
+ 'var boxes=document.querySelectorAll(".pin-digit");' +
1025
+ 'for(var i=0;i<boxes.length;i++)boxes[i].addEventListener("input",function(){btns[2].disabled=pinEl.value.length!==6})}' +
1026
+ '}' +
1027
+
1028
+ // Step 1 validation (username)
1029
+ 'usernameEl.addEventListener("input",function(){btns[0].disabled=usernameEl.value.trim().length<1});' +
1030
+ 'usernameEl.addEventListener("keydown",function(e){if(e.key==="Enter"&&!btns[0].disabled)goStep(1)});' +
1031
+ 'btns[0].onclick=function(){goStep(1)};' +
1032
+
1033
+ // Step 2 validation (display name)
1034
+ 'dnEl.addEventListener("input",function(){btns[1].disabled=dnEl.value.trim().length<1});' +
1035
+ 'dnEl.addEventListener("keydown",function(e){if(e.key==="Enter"&&!btns[1].disabled)goStep(2)});' +
1036
+ 'btns[1].onclick=function(){goStep(2)};' +
1037
+
1038
+ 'function doRegister(){' +
1039
+ 'btns[2].disabled=true;errs[2].textContent="";' +
1040
+ 'fetch("/auth/register",{method:"POST",headers:{"Content-Type":"application/json"},' +
1041
+ 'body:JSON.stringify({inviteCode:inviteCode,username:usernameEl.value.trim(),displayName:dnEl.value.trim(),pin:pinEl.value})})' +
1042
+ '.then(function(r){return r.json()})' +
1043
+ '.then(function(d){' +
1044
+ 'if(d.ok){location.href="/";return}' +
1045
+ 'errs[2].textContent=d.error||"Registration failed";btns[2].disabled=false})' +
1046
+ '.catch(function(){errs[2].textContent="Connection error";btns[2].disabled=false})}' +
1047
+ 'btns[2].onclick=doRegister;' +
1048
+ '</script></div></body></html>';
1049
+ }
1050
+
1051
+ // --- SMTP OTP Login Page (2-step wizard: email → OTP code) ---
1052
+ function smtpLoginPageHtml() {
1053
+ return '<!DOCTYPE html><html lang="en"><head>' +
1054
+ '<meta charset="UTF-8">' +
1055
+ '<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">' +
1056
+ '<title>Login - Clay</title>' +
1057
+ '<style>' + authPageStyles +
1058
+ '.otp-input{width:100%;font-size:24px;letter-spacing:8px;text-align:center;padding:12px;' +
1059
+ 'background:var(--field-bg);border:1px solid var(--field-border);border-radius:8px;color:var(--fg);' +
1060
+ 'font-family:monospace;outline:none;box-sizing:border-box}' +
1061
+ '.otp-input:focus{border-color:var(--accent)}' +
1062
+ '.resend-row{text-align:center;margin-top:12px;font-size:13px;color:var(--muted)}' +
1063
+ '.resend-link{color:var(--accent);cursor:pointer;text-decoration:underline;background:none;border:none;font:inherit}' +
1064
+ '.resend-link:disabled{color:var(--muted);cursor:default;text-decoration:none}' +
1065
+ '</style></head><body><div class="c">' +
1066
+ '<div class="steps-bar"><span class="steps-dot current" id="dot0"></span><span class="steps-dot" id="dot1"></span></div>' +
1067
+
1068
+ // Step 1: Email
1069
+ '<div class="step active" id="step0">' +
1070
+ '<h1>Welcome back</h1>' +
1071
+ '<div class="sub">Enter your email to receive a login code</div>' +
1072
+ '<div class="field"><label>Email</label>' +
1073
+ '<input id="email" type="email" maxlength="100" placeholder="you@example.com" autocomplete="email" autofocus></div>' +
1074
+ '<button class="btn" id="btn0" disabled>Send Code</button>' +
1075
+ '<div class="err" id="err0"></div>' +
1076
+ '</div>' +
1077
+
1078
+ // Step 2: OTP Code
1079
+ '<div class="step" id="step1">' +
1080
+ '<h1>Check your inbox</h1>' +
1081
+ '<div class="sub">Enter the 6-digit code sent to your email</div>' +
1082
+ '<input class="otp-input" id="otp" type="tel" maxlength="6" inputmode="numeric" placeholder="000000" autocomplete="one-time-code">' +
1083
+ '<button class="btn" id="btn1" disabled style="margin-top:20px">Log In</button>' +
1084
+ '<div class="resend-row"><button class="resend-link" id="resend" disabled>Resend code (<span id="countdown">60</span>s)</button></div>' +
1085
+ '<div class="err" id="err1"></div>' +
1086
+ '</div>' +
1087
+
1088
+ // PIN fallback (hidden username+PIN form)
1089
+ '<div id="pin-fallback" style="display:none">' +
1090
+ '<h1>Log in with PIN</h1>' +
1091
+ '<div class="sub">Use your username and PIN instead</div>' +
1092
+ '<div class="field"><label>Username</label>' +
1093
+ '<input id="fb-username" type="text" maxlength="100" placeholder="Username" autocomplete="username"></div>' +
1094
+ '<div class="field" style="margin-top:12px"><label>PIN</label>' +
1095
+ '<input id="fb-pin" type="password" maxlength="6" inputmode="numeric" placeholder="6-digit PIN" autocomplete="current-password"></div>' +
1096
+ '<button class="btn" id="fb-btn" disabled style="margin-top:16px">Log In</button>' +
1097
+ '<div class="err" id="fb-err"></div>' +
1098
+ '<div class="resend-row"><button class="resend-link" id="fb-back">Back to email login</button></div>' +
1099
+ '</div>' +
1100
+
1101
+ '<div class="resend-row" id="pin-link-row"><button class="resend-link" id="pin-link">Log in with PIN instead</button></div>' +
1102
+
1103
+ '<script>' +
1104
+ 'var step=0,cooldown=0,cooldownTimer=null;' +
1105
+ 'var emailEl=document.getElementById("email"),otpEl=document.getElementById("otp");' +
1106
+ 'var steps=[document.getElementById("step0"),document.getElementById("step1")];' +
1107
+ 'var dots=[document.getElementById("dot0"),document.getElementById("dot1")];' +
1108
+ 'var btns=[document.getElementById("btn0"),document.getElementById("btn1")];' +
1109
+ 'var errs=[document.getElementById("err0"),document.getElementById("err1")];' +
1110
+ 'var resendBtn=document.getElementById("resend"),cdSpan=document.getElementById("countdown");' +
1111
+
1112
+ 'function goStep(n){' +
1113
+ 'steps[step].classList.remove("active");dots[step].classList.remove("current");dots[step].classList.add("done");' +
1114
+ 'step=n;steps[step].classList.add("active");dots[step].classList.add("current");' +
1115
+ 'errs[step].textContent="";' +
1116
+ 'if(step===1){otpEl.value="";otpEl.focus()}' +
1117
+ '}' +
1118
+
1119
+ 'function startCooldown(){' +
1120
+ 'cooldown=60;resendBtn.disabled=true;' +
1121
+ 'cdSpan.textContent=cooldown;' +
1122
+ 'if(cooldownTimer)clearInterval(cooldownTimer);' +
1123
+ 'cooldownTimer=setInterval(function(){cooldown--;cdSpan.textContent=cooldown;' +
1124
+ 'if(cooldown<=0){clearInterval(cooldownTimer);cooldownTimer=null;resendBtn.disabled=false;' +
1125
+ 'resendBtn.innerHTML="Resend code"}},1000)}' +
1126
+
1127
+ // Step 1: email validation + send OTP
1128
+ 'emailEl.addEventListener("input",function(){btns[0].disabled=emailEl.value.length<1});' +
1129
+ 'emailEl.addEventListener("keydown",function(e){if(e.key==="Enter"&&!btns[0].disabled)requestCode()});' +
1130
+
1131
+ 'function requestCode(){' +
1132
+ 'btns[0].disabled=true;errs[0].textContent="";' +
1133
+ 'fetch("/auth/request-otp",{method:"POST",headers:{"Content-Type":"application/json"},' +
1134
+ 'body:JSON.stringify({email:emailEl.value})})' +
1135
+ '.then(function(r){return r.json()})' +
1136
+ '.then(function(d){' +
1137
+ 'if(d.ok){goStep(1);startCooldown();return}' +
1138
+ 'if(d.locked){errs[0].textContent="Too many attempts. Try again in "+Math.ceil(d.retryAfter/60)+" min";return}' +
1139
+ 'errs[0].textContent=d.error||"Failed to send code";btns[0].disabled=false})' +
1140
+ '.catch(function(){errs[0].textContent="Connection error";btns[0].disabled=false})}' +
1141
+ 'btns[0].onclick=requestCode;' +
1142
+
1143
+ // Step 2: OTP validation
1144
+ 'otpEl.addEventListener("input",function(){' +
1145
+ 'var v=this.value.replace(/[^0-9]/g,"");if(v.length>6)v=v.slice(0,6);this.value=v;' +
1146
+ 'btns[1].disabled=v.length!==6;' +
1147
+ 'if(v.length===6)doLogin()});' +
1148
+ 'otpEl.addEventListener("keydown",function(e){if(e.key==="Enter"&&!btns[1].disabled)doLogin()});' +
1149
+
1150
+ // Resend
1151
+ 'resendBtn.onclick=function(){' +
1152
+ 'resendBtn.disabled=true;' +
1153
+ 'fetch("/auth/request-otp",{method:"POST",headers:{"Content-Type":"application/json"},' +
1154
+ 'body:JSON.stringify({email:emailEl.value})})' +
1155
+ '.then(function(r){return r.json()})' +
1156
+ '.then(function(d){if(d.ok){startCooldown();errs[1].textContent=""}' +
1157
+ 'else{errs[1].textContent=d.error||"Failed to resend";resendBtn.disabled=false}})' +
1158
+ '.catch(function(){errs[1].textContent="Connection error";resendBtn.disabled=false})};' +
1159
+
1160
+ 'function doLogin(){' +
1161
+ 'btns[1].disabled=true;errs[1].textContent="";' +
1162
+ 'fetch("/auth/verify-otp",{method:"POST",headers:{"Content-Type":"application/json"},' +
1163
+ 'body:JSON.stringify({email:emailEl.value,code:otpEl.value})})' +
1164
+ '.then(function(r){return r.json()})' +
1165
+ '.then(function(d){' +
1166
+ 'if(d.ok){location.reload();return}' +
1167
+ 'if(d.locked){otpEl.disabled=true;' +
1168
+ 'errs[1].textContent="Too many attempts. Try again in "+Math.ceil(d.retryAfter/60)+" min";' +
1169
+ 'setTimeout(function(){otpEl.disabled=false;otpEl.value="";btns[1].disabled=true;otpEl.focus()},d.retryAfter*1000);return}' +
1170
+ 'var msg=d.error||"Invalid code";' +
1171
+ 'if(typeof d.attemptsLeft==="number")msg+=" ("+d.attemptsLeft+" left)";' +
1172
+ 'errs[1].textContent=msg;otpEl.value="";btns[1].disabled=true;otpEl.focus()})' +
1173
+ '.catch(function(){errs[1].textContent="Connection error";btns[1].disabled=false})}' +
1174
+ 'btns[1].onclick=doLogin;' +
1175
+
1176
+ // PIN fallback logic
1177
+ 'var pinFb=document.getElementById("pin-fallback"),pinLinkRow=document.getElementById("pin-link-row");' +
1178
+ 'var fbUser=document.getElementById("fb-username"),fbPin=document.getElementById("fb-pin");' +
1179
+ 'var fbBtn=document.getElementById("fb-btn"),fbErr=document.getElementById("fb-err");' +
1180
+ 'document.getElementById("pin-link").onclick=function(){' +
1181
+ 'steps[step].classList.remove("active");dots[step].classList.remove("current");' +
1182
+ 'pinFb.style.display="block";pinLinkRow.style.display="none";' +
1183
+ 'document.querySelector(".steps-bar").style.display="none";fbUser.focus()};' +
1184
+ 'document.getElementById("fb-back").onclick=function(){' +
1185
+ 'pinFb.style.display="none";pinLinkRow.style.display="";' +
1186
+ 'document.querySelector(".steps-bar").style.display="";' +
1187
+ 'step=0;steps[0].classList.add("active");dots[0].classList.add("current");emailEl.focus()};' +
1188
+ 'function checkFb(){fbBtn.disabled=!(fbUser.value.length>0&&fbPin.value.length===6)}' +
1189
+ 'fbUser.addEventListener("input",checkFb);fbPin.addEventListener("input",checkFb);' +
1190
+ 'function doPinLogin(){' +
1191
+ 'fbBtn.disabled=true;fbErr.textContent="";' +
1192
+ 'fetch("/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},' +
1193
+ 'body:JSON.stringify({username:fbUser.value,pin:fbPin.value})})' +
1194
+ '.then(function(r){return r.json()})' +
1195
+ '.then(function(d){' +
1196
+ 'if(d.ok){location.reload();return}' +
1197
+ 'var msg=d.error||"Invalid credentials";' +
1198
+ 'if(typeof d.attemptsLeft==="number"&&d.attemptsLeft<=3)msg+=" ("+d.attemptsLeft+" left)";' +
1199
+ 'fbErr.textContent=msg;fbPin.value="";fbBtn.disabled=true;fbPin.focus()})' +
1200
+ '.catch(function(){fbErr.textContent="Connection error";fbBtn.disabled=false})}' +
1201
+ 'fbBtn.onclick=doPinLogin;' +
1202
+ 'fbPin.addEventListener("keydown",function(e){if(e.key==="Enter"&&!fbBtn.disabled)doPinLogin()});' +
1203
+
1204
+ '</script></div></body></html>';
1205
+ }
1206
+
1207
+ // --- SMTP Invite Registration Page (2-step wizard: email → display name, no PIN) ---
1208
+ function smtpInvitePageHtml(inviteCode) {
1209
+ return '<!DOCTYPE html><html lang="en"><head>' +
1210
+ '<meta charset="UTF-8">' +
1211
+ '<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">' +
1212
+ '<title>Join - Clay</title>' +
1213
+ '<style>' + authPageStyles + '</style></head><body><div class="c">' +
1214
+ '<div class="steps-bar"><span class="steps-dot current" id="dot0"></span><span class="steps-dot" id="dot1"></span><span class="steps-dot" id="dot2"></span></div>' +
1215
+
1216
+ // Step 1: Username
1217
+ '<div class="step active" id="step0">' +
1218
+ '<h1>You&#39;re invited!</h1>' +
1219
+ '<div class="sub">Pick a username to get started</div>' +
1220
+ '<div class="field"><label>Username</label>' +
1221
+ '<input id="username" type="text" maxlength="100" placeholder="Username" autocomplete="username" autofocus></div>' +
1222
+ '<button class="btn" id="btn0" disabled>Continue</button>' +
1223
+ '<div class="err" id="err0"></div>' +
1224
+ '</div>' +
1225
+
1226
+ // Step 2: Email
1227
+ '<div class="step" id="step1">' +
1228
+ '<h1>Add your email</h1>' +
1229
+ '<div class="sub">You&#39;ll use this to log in later</div>' +
1230
+ '<div class="field"><label>Email</label>' +
1231
+ '<input id="email" type="email" maxlength="100" placeholder="you@example.com" autocomplete="email"></div>' +
1232
+ '<button class="btn" id="btn1" disabled>Continue</button>' +
1233
+ '<div class="err" id="err1"></div>' +
1234
+ '</div>' +
1235
+
1236
+ // Step 3: Display Name
1237
+ '<div class="step" id="step2">' +
1238
+ '<h1>What should we call you?</h1>' +
1239
+ '<div class="sub">Your display name is shown in conversations</div>' +
1240
+ '<div class="field"><label>Display Name</label>' +
1241
+ '<input id="displayname" type="text" maxlength="30" placeholder="Your name" autocomplete="name"></div>' +
1242
+ '<button class="btn" id="btn2" disabled>Create Account</button>' +
1243
+ '<div class="err" id="err2"></div>' +
1244
+ '</div>' +
1245
+
1246
+ '<script>' +
1247
+ 'var inviteCode=' + JSON.stringify(inviteCode) + ';' +
1248
+ 'var step=0;' +
1249
+ 'var usernameEl=document.getElementById("username"),emailEl=document.getElementById("email"),dnEl=document.getElementById("displayname");' +
1250
+ 'var steps=[document.getElementById("step0"),document.getElementById("step1"),document.getElementById("step2")];' +
1251
+ 'var dots=[document.getElementById("dot0"),document.getElementById("dot1"),document.getElementById("dot2")];' +
1252
+ 'var btns=[document.getElementById("btn0"),document.getElementById("btn1"),document.getElementById("btn2")];' +
1253
+ 'var errs=[document.getElementById("err0"),document.getElementById("err1"),document.getElementById("err2")];' +
1254
+ 'var emailRe=/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;' +
1255
+
1256
+ 'function goStep(n){' +
1257
+ 'steps[step].classList.remove("active");dots[step].classList.remove("current");dots[step].classList.add("done");' +
1258
+ 'step=n;steps[step].classList.add("active");dots[step].classList.add("current");' +
1259
+ 'errs[step].textContent="";' +
1260
+ 'if(step===1)emailEl.focus();' +
1261
+ 'if(step===2){dnEl.focus();if(!dnEl.value)dnEl.value=usernameEl.value}' +
1262
+ '}' +
1263
+
1264
+ // Step 1 validation (username)
1265
+ 'usernameEl.addEventListener("input",function(){btns[0].disabled=usernameEl.value.trim().length<1});' +
1266
+ 'usernameEl.addEventListener("keydown",function(e){if(e.key==="Enter"&&!btns[0].disabled)goStep(1)});' +
1267
+ 'btns[0].onclick=function(){goStep(1)};' +
1268
+
1269
+ // Step 2 validation (email)
1270
+ 'emailEl.addEventListener("input",function(){' +
1271
+ 'var v=emailEl.value;var valid=emailRe.test(v);' +
1272
+ 'btns[1].disabled=!valid;' +
1273
+ 'if(v.length>0&&!valid)errs[1].textContent="Enter a valid email address";' +
1274
+ 'else errs[1].textContent=""});' +
1275
+ 'emailEl.addEventListener("keydown",function(e){if(e.key==="Enter"&&!btns[1].disabled)goStep(2)});' +
1276
+ 'btns[1].onclick=function(){goStep(2)};' +
1277
+
1278
+ // Step 3 validation (display name)
1279
+ 'dnEl.addEventListener("input",function(){btns[2].disabled=dnEl.value.trim().length<1});' +
1280
+ 'dnEl.addEventListener("keydown",function(e){if(e.key==="Enter"&&!btns[2].disabled)doRegister()});' +
1281
+
1282
+ 'function doRegister(){' +
1283
+ 'btns[2].disabled=true;errs[2].textContent="";' +
1284
+ 'fetch("/auth/register",{method:"POST",headers:{"Content-Type":"application/json"},' +
1285
+ 'body:JSON.stringify({inviteCode:inviteCode,username:usernameEl.value.trim(),email:emailEl.value,displayName:dnEl.value.trim()})})' +
1286
+ '.then(function(r){return r.json()})' +
1287
+ '.then(function(d){' +
1288
+ 'if(d.ok){location.href="/";return}' +
1289
+ 'errs[2].textContent=d.error||"Registration failed";btns[2].disabled=false})' +
1290
+ '.catch(function(){errs[2].textContent="Connection error";btns[2].disabled=false})}' +
1291
+ 'btns[2].onclick=doRegister;' +
1292
+ '</script></div></body></html>';
1293
+ }
1294
+
1295
+ // --- No Projects Assigned Page (multi-user: user has no accessible projects) ---
1296
+ function noProjectsPageHtml() {
1297
+ return '<!DOCTYPE html><html lang="en"><head>' +
1298
+ '<meta charset="UTF-8">' +
1299
+ '<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">' +
1300
+ '<title>Clay</title>' +
1301
+ '<style>' + authPageStyles + '</style></head><body><div class="c">' +
1302
+ '<h1>Hang tight!</h1>' +
1303
+ '<div class="sub">No projects have been assigned to your account yet.</div>' +
1304
+ '<div class="info">Ask an admin to grant you access to a project.</div>' +
1305
+ '</div></body></html>';
1306
+ }
1307
+
1308
+ module.exports = { pinPageHtml: pinPageHtml, setupPageHtml: setupPageHtml, adminSetupPageHtml: adminSetupPageHtml, multiUserLoginPageHtml: multiUserLoginPageHtml, smtpLoginPageHtml: smtpLoginPageHtml, invitePageHtml: invitePageHtml, smtpInvitePageHtml: smtpInvitePageHtml, noProjectsPageHtml: noProjectsPageHtml };