clay-server 2.8.2 → 2.9.0

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