somratpro Claude Sonnet 4.6 commited on
Commit
5e5b81c
·
1 Parent(s): b6c7f2b

setup: redesign to match dashboard design system + fix X docs

Browse files

- Match CSS vars exactly to dashboard (--bg:#08080f, --panel, --line,
Inter font, font-weight:850 labels, 8px border-radius cards)
- Sidebar: dot indicators (green/amber/grey) replace emoji badges
- Steps: gradient numbered circles, proper strong/code/link styling
- Callback/env blocks: monospace text, accent-coloured copy buttons
- Settings CTA: gradient button matching dashboard hero-action style
- X/Twitter steps: rewritten with correct OAuth 1.0 Keys instructions
(Consumer Key = X_API_KEY, Consumer Secret via Regenerate popup,
Native App required, Web App causes error 32)
- All callback URLs already correct: /integrations/social/{provider}

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. health-server.js +440 -166
health-server.js CHANGED
@@ -285,19 +285,23 @@ function getOAuthPlatformDetails(publicUrl) {
285
  steps: [
286
  {
287
  title: "Create an X Developer App",
288
- body: 'Apply for a developer account at <a href="https://developer.twitter.com" target="_blank" rel="noopener" style="color:#f472b6">developer.twitter.com</a> if you don\'t have one. Create a new project + app.',
289
  },
290
  {
291
- title: "Enable OAuth 1.0a + set permissions",
292
- body: "On your app page → <strong>User authentication settings → Set up</strong>. Enable <strong>OAuth 1.0a</strong>. Set App permissions to <strong>Read and Write</strong>. Set Type of App to <strong>Native App</strong> (⚠️ must be Native App, not Web App — Web App breaks OAuth 1.0a).",
293
  },
294
  {
295
- title: "Add callback URL",
296
- body: "In the same setup screen, under <strong>Callback URI / Redirect URL</strong>, paste the Callback URL shown below.",
 
 
 
 
297
  },
298
  {
299
- title: "Get your Consumer Secret",
300
- body: "<strong>⚠️ The Consumer Secret (X_API_SECRET) is only shown once</strong> right after app creation, or after you click <strong>Regenerate</strong> on the Consumer Key row in the Keys &amp; Tokens tab.<br><br>If you don't have it saved: go to <strong>Keys &amp; Tokens OAuth 1.0 Keys Regenerate</strong>. Copy <em>both</em> the new Consumer Key and Consumer Secret that appear in the popup.",
301
  },
302
  {
303
  title: "Add to Space secrets",
@@ -708,7 +712,7 @@ function getOAuthPlatformDetails(publicUrl) {
708
 
709
  function renderSetupPage() {
710
  const spaceHost = process.env.SPACE_HOST || null;
711
- const spaceId = process.env.SPACE_ID || null;
712
  const publicUrl = spaceHost
713
  ? `https://${spaceHost}`
714
  : "http://localhost:7860";
@@ -722,180 +726,456 @@ function renderSetupPage() {
722
  ).length;
723
 
724
  // Build sidebar items
725
- const sidebarItems = platforms
726
- .map((p, i) => {
727
- const allSet = p.envVars.every((v) => v.set);
728
- const anySet = p.envVars.some((v) => v.set);
729
- const indicator = allSet ? "✅" : anySet ? "⚠️" : "⚪";
730
- return `<button class="plat-tab${i === 0 ? " active" : ""}" onclick="show(${i})" id="tab-${i}">
 
 
 
731
  <span class="tab-emoji">${p.emoji}</span>
732
  <span class="tab-name">${p.name}</span>
733
- <span class="tab-indicator">${indicator}</span>
734
  </button>`;
735
- })
736
- .join("");
737
 
738
  // Build detail panels
739
- const panels = platforms
740
- .map((p, i) => {
741
- const allSet = p.envVars.every((v) => v.set);
742
-
743
- const stepsList = p.steps
744
- .map(
745
- (s, si) =>
746
- `<div class="step"><div class="step-num">${si + 1}</div><div><div class="step-title">${s.title}</div><div class="step-body">${s.body}</div></div></div>`,
747
- )
748
- .join("");
749
-
750
- const envRows = p.envVars
751
- .map(
752
- (v) =>
753
- `<div class="env-row">
 
754
  <div class="env-info">
755
  <code class="env-name">${v.name}</code>
756
  <span class="env-desc">${v.desc}</span>
757
  </div>
758
  <div class="env-actions">
759
- ${v.set ? '<span class="badge badge-on" style="font-size:.7rem">Set ✓</span>' : '<span class="badge badge-off" style="font-size:.7rem">Not set</span>'}
760
- <button class="copy-btn" onclick="copy('${v.name}', this)">Copy name</button>
 
 
761
  </div>
762
- </div>`,
763
- )
764
- .join("");
765
-
766
- const statusBanner = allSet
767
- ? `<div class="status-banner banner-ok">✅ All credentials configured — restart Space if you just added them.</div>`
768
- : p.envVars.some((v) => v.set)
769
- ? `<div class="status-banner banner-warn">⚠️ Partially configured — check missing env vars below.</div>`
770
- : `<div class="status-banner banner-info">ℹ️ Not yet configured — follow the steps below.</div>`;
771
-
772
- return `<div class="panel${i === 0 ? " active" : ""}" id="panel-${i}">
773
- <div class="panel-header">
774
  <span class="panel-emoji">${p.emoji}</span>
775
  <div>
776
- <h2 class="panel-title">${p.name}</h2>
777
- <a class="portal-link" href="${p.setupUrl}" target="_blank" rel="noopener">Open ${p.name} Developer Portal →</a>
778
- ${p.docsUrl ? `<a class="portal-link" href="${p.docsUrl}" target="_blank" rel="noopener" style="margin-left:12px">Docs →</a>` : ""}
 
 
779
  </div>
780
  </div>
781
 
782
- ${statusBanner}
783
 
784
- <h3 class="section-label">Setup Steps</h3>
785
  <div class="steps-list">${stepsList}</div>
786
 
787
- <h3 class="section-label">Callback URL</h3>
 
 
788
  <div class="copy-block">
789
- <span class="copy-block-text" id="cb-${i}">${p.callbackUrl}</span>
790
- <button class="copy-btn copy-btn-primary" onclick="copy('${p.callbackUrl}', this)">Copy</button>
791
  </div>
792
- <p class="hint">Paste this URL wherever the developer portal asks for "Redirect URI", "Callback URL", or "OAuth Redirect URL".</p>
793
 
794
- <h3 class="section-label">Space Secrets to Add</h3>
795
- <div class="env-list">${envRows}</div>
796
- <div class="settings-cta">
797
- <a href="${settingsUrl}" target="_blank" rel="noopener" class="settings-btn">Open Space Settings → Variables &amp; Secrets</a>
798
- <p class="hint">After adding secrets, click <strong>Restart Space</strong> for them to take effect.</p>
799
  </div>
 
 
 
 
 
 
800
  </div>`;
801
- })
802
- .join("");
803
 
804
  return `<!DOCTYPE html>
805
  <html lang="en">
806
  <head>
807
  <meta charset="UTF-8">
808
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
809
  <title>Platform Setup — HuggingPost</title>
810
- <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&display=swap" rel="stylesheet">
811
  <style>
812
- :root{--bg:#0f172a;--sidebar:#0d1829;--card:rgba(30,41,59,.75);--border:rgba(255,255,255,.08);--accent:linear-gradient(135deg,#ec4899,#8b5cf6);--text:#f8fafc;--dim:#94a3b8;--ok:#10b981;--warn:#f59e0b;--err:#ef4444;--blue:#3b82f6;--pink:#f472b6}
813
- *{box-sizing:border-box;margin:0;padding:0}
814
- body{font-family:'Outfit',sans-serif;background:var(--bg);color:var(--text);height:100vh;display:flex;flex-direction:column;overflow:hidden;
815
- background-image:radial-gradient(at 0% 0%,rgba(236,72,153,.12) 0,transparent 50%),radial-gradient(at 100% 100%,rgba(139,92,246,.12) 0,transparent 50%)}
816
- /* Top bar */
817
- .topbar{display:flex;align-items:center;gap:16px;padding:14px 20px;border-bottom:1px solid var(--border);background:rgba(15,23,42,.8);backdrop-filter:blur(8px);flex-shrink:0}
818
- .topbar a{color:var(--dim);text-decoration:none;font-size:.85rem;display:flex;align-items:center;gap:6px}
819
- .topbar a:hover{color:var(--text)}
820
- .topbar h1{font-size:1.1rem;font-weight:600;background:var(--accent);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
821
- .topbar-right{margin-left:auto;font-size:.8rem;color:var(--dim)}
822
- /* Layout */
823
- .layout{display:flex;flex:1;overflow:hidden}
824
- /* Sidebar */
825
- .sidebar{width:220px;flex-shrink:0;background:var(--sidebar);border-right:1px solid var(--border);overflow-y:auto;padding:12px 8px}
826
- .sidebar-label{font-size:.65rem;text-transform:uppercase;color:var(--dim);letter-spacing:.1em;padding:4px 10px 8px}
827
- .plat-tab{width:100%;background:none;border:none;color:var(--text);font:inherit;font-size:.88rem;display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:10px;cursor:pointer;text-align:left;transition:background .15s}
828
- .plat-tab:hover{background:rgba(255,255,255,.05)}
829
- .plat-tab.active{background:rgba(236,72,153,.12);color:var(--pink)}
830
- .tab-emoji{font-size:1rem;width:22px;text-align:center;flex-shrink:0}
831
- .tab-name{flex:1}
832
- .tab-indicator{font-size:.8rem}
833
- /* Main panel */
834
- .main{flex:1;overflow-y:auto;padding:28px 32px}
835
- .panel{display:none;animation:fadein .2s ease}
836
- .panel.active{display:block}
837
- @keyframes fadein{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
838
- .panel-header{display:flex;align-items:flex-start;gap:16px;margin-bottom:20px}
839
- .panel-emoji{font-size:2.5rem;flex-shrink:0;margin-top:2px}
840
- .panel-title{font-size:1.5rem;font-weight:600;margin-bottom:4px}
841
- .portal-link{color:var(--pink);font-size:.82rem;text-decoration:none}
842
- .portal-link:hover{text-decoration:underline}
843
- /* Status banner */
844
- .status-banner{padding:10px 14px;border-radius:10px;font-size:.85rem;margin-bottom:20px}
845
- .banner-ok{background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.2);color:#6ee7b7}
846
- .banner-warn{background:rgba(245,158,11,.1);border:1px solid rgba(245,158,11,.2);color:#fcd34d}
847
- .banner-info{background:rgba(59,130,246,.1);border:1px solid rgba(59,130,246,.2);color:#93c5fd}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
848
  /* Section labels */
849
- .section-label{font-size:.7rem;text-transform:uppercase;letter-spacing:.1em;color:var(--dim);margin:20px 0 10px}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
850
  /* Steps */
851
- .steps-list{display:flex;flex-direction:column;gap:2px}
852
- .step{display:flex;gap:12px;padding:10px 12px;border-radius:10px;background:rgba(255,255,255,.03);border:1px solid var(--border)}
853
- .step-num{width:24px;height:24px;border-radius:50%;background:var(--accent);color:#fff;font-size:.7rem;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px}
854
- .step-title{font-size:.88rem;font-weight:600;margin-bottom:3px}
855
- .step-body{font-size:.82rem;color:var(--dim);line-height:1.55}
856
- .step-body code{background:rgba(255,255,255,.1);padding:1px 5px;border-radius:4px;font-size:.8em;color:var(--text)}
857
- /* Callback URL copy block */
858
- .copy-block{display:flex;align-items:center;gap:10px;background:rgba(0,0,0,.3);border:1px solid var(--border);border-radius:10px;padding:12px 14px;margin-bottom:6px}
859
- .copy-block-text{flex:1;font-size:.82rem;color:#c4b5fd;word-break:break-all;font-family:monospace}
860
- /* Env vars */
861
- .env-list{display:flex;flex-direction:column;gap:6px;margin-bottom:16px}
862
- .env-row{display:flex;align-items:center;gap:10px;padding:10px 14px;background:rgba(255,255,255,.03);border:1px solid var(--border);border-radius:10px}
863
- .env-info{flex:1;display:flex;flex-direction:column;gap:3px}
864
- .env-name{font-size:.82rem;color:#c4b5fd;background:rgba(139,92,246,.1);padding:2px 7px;border-radius:5px;width:fit-content}
865
- .env-desc{font-size:.76rem;color:var(--dim)}
866
- .env-actions{display:flex;align-items:center;gap:8px;flex-shrink:0}
867
- /* Buttons */
868
- .copy-btn{background:rgba(255,255,255,.07);border:1px solid var(--border);color:var(--text);font:inherit;font-size:.75rem;padding:5px 10px;border-radius:7px;cursor:pointer;transition:background .15s;flex-shrink:0}
869
- .copy-btn:hover{background:rgba(255,255,255,.12)}
870
- .copy-btn.copied{background:rgba(16,185,129,.15);border-color:rgba(16,185,129,.3);color:var(--ok)}
871
- .copy-btn-primary{background:rgba(236,72,153,.15);border-color:rgba(236,72,153,.3);color:var(--pink);font-size:.82rem;padding:6px 14px}
872
- .copy-btn-primary:hover{background:rgba(236,72,153,.25)}
873
- .settings-btn{display:inline-block;background:var(--accent);color:#fff;text-decoration:none;padding:11px 20px;border-radius:10px;font-size:.88rem;font-weight:600;transition:opacity .2s}
874
- .settings-btn:hover{opacity:.85}
875
- .settings-cta{margin-top:4px}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
876
  /* Badges */
877
- .badge{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:20px;font-weight:600}
878
- .badge-on{background:rgba(16,185,129,.12);color:var(--ok)}
879
- .badge-off{background:rgba(239,68,68,.12);color:var(--err)}
880
- /* Hint */
881
- .hint{font-size:.78rem;color:var(--dim);margin-top:6px;line-height:1.5;margin-bottom:16px}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
882
  /* Mobile */
883
- @media(max-width:700px){
884
- body{overflow:auto;height:auto}
885
- .layout{flex-direction:column;overflow:visible}
886
- .sidebar{width:100%;border-right:none;border-bottom:1px solid var(--border);display:flex;flex-wrap:wrap;padding:8px;gap:4px}
887
- .sidebar-label{display:none}
888
- .plat-tab{width:auto;flex:0 0 auto;padding:6px 10px}
889
- .tab-name{display:none}
890
- .main{padding:16px}
 
 
 
 
 
 
 
 
 
891
  }
892
  </style>
893
  </head>
894
  <body>
895
  <div class="topbar">
896
- <a href="/">← Dashboard</a>
897
- <h1>Platform Setup Guide</h1>
898
- <span class="topbar-right">${configuredCount}/${platforms.length} configured</span>
899
  </div>
900
  <div class="layout">
901
  <nav class="sidebar">
@@ -909,36 +1189,30 @@ body{font-family:'Outfit',sans-serif;background:var(--bg);color:var(--text);heig
909
  <script>
910
  const PLATFORM_IDS = ${JSON.stringify(platforms.map((p) => p.id))};
911
  function show(i) {
912
- document.querySelectorAll('.plat-tab').forEach((t,j) => t.classList.toggle('active', j===i));
913
- document.querySelectorAll('.panel').forEach((p,j) => p.classList.toggle('active', j===i));
914
- if (PLATFORM_IDS[i]) history.replaceState(null, '', '#' + PLATFORM_IDS[i]);
915
  }
916
- function copy(text, btn) {
917
- navigator.clipboard.writeText(text).then(() => {
918
  const orig = btn.textContent;
919
  btn.textContent = 'Copied!';
920
  btn.classList.add('copied');
921
- setTimeout(() => { btn.textContent = orig; btn.classList.remove('copied'); }, 1800);
922
- }).catch(() => {
923
- // fallback for non-HTTPS
924
- const ta = document.createElement('textarea');
925
- ta.value = text; ta.style.position='fixed'; ta.style.opacity='0';
 
 
926
  document.body.appendChild(ta); ta.select();
927
- try { document.execCommand('copy'); } catch {}
928
- document.body.removeChild(ta);
929
- const orig = btn.textContent;
930
- btn.textContent = 'Copied!';
931
- btn.classList.add('copied');
932
- setTimeout(() => { btn.textContent = orig; btn.classList.remove('copied'); }, 1800);
933
- });
934
- }
935
- // Hash-based deep-linking: /setup#linkedin jumps to LinkedIn tab
936
- (function() {
937
- const hash = location.hash.replace('#','').toLowerCase();
938
- if (hash) {
939
- const idx = PLATFORM_IDS.indexOf(hash);
940
- if (idx !== -1) show(idx);
941
  }
 
 
 
 
942
  })();
943
  </script>
944
  </body>
 
285
  steps: [
286
  {
287
  title: "Create an X Developer App",
288
+ body: 'Go to <a href="https://developer.twitter.com" target="_blank" rel="noopener">developer.twitter.com</a>. Apply for a developer account if needed. Create a new project + app.',
289
  },
290
  {
291
+ title: "Set permissions to Read and write",
292
+ body: "On your app page → <strong>User authentication settings → Set up</strong>. Under <strong>App permissions</strong>, select <strong>Read and write</strong>.",
293
  },
294
  {
295
+ title: "Set Type of App to Native App",
296
+ body: "<strong>⚠️ Critical:</strong> Under <strong>Type of App</strong>, select <strong>Native App</strong> (Public client). Do <em>not</em> select Web App Web App causes OAuth error code 32 and authentication will fail.",
297
+ },
298
+ {
299
+ title: "Add callback URL and website URL",
300
+ body: "Under <strong>Callback URI / Redirect URL</strong>, paste the Callback URL shown below. Also set Website URL to your HF Space URL.",
301
  },
302
  {
303
+ title: "Copy Consumer Key and Consumer Secret",
304
+ body: "Go to your app <strong>Keys &amp; Tokens</strong> tab <strong>OAuth 1.0 Keys</strong> section.<br><br><strong>X_API_KEY</strong> = Consumer Key (click Show)<br><strong>X_API_SECRET</strong> = Consumer Secret click <strong>Regenerate</strong> to reveal both together (the secret is only shown in the Regenerate popup).<br><br>⚠️ Do <em>not</em> use Bearer Token, Access Token, or OAuth 2.0 Client ID/Secret.",
305
  },
306
  {
307
  title: "Add to Space secrets",
 
712
 
713
  function renderSetupPage() {
714
  const spaceHost = process.env.SPACE_HOST || null;
715
+ const spaceId = process.env.SPACE_ID || null;
716
  const publicUrl = spaceHost
717
  ? `https://${spaceHost}`
718
  : "http://localhost:7860";
 
726
  ).length;
727
 
728
  // Build sidebar items
729
+ const sidebarItems = platforms.map((p, i) => {
730
+ const allSet = p.envVars.every((v) => v.set);
731
+ const anySet = p.envVars.some((v) => v.set);
732
+ const dot = allSet
733
+ ? `<span class="dot dot-ok"></span>`
734
+ : anySet
735
+ ? `<span class="dot dot-warn"></span>`
736
+ : `<span class="dot dot-off"></span>`;
737
+ return `<button class="plat-tab${i === 0 ? " active" : ""}" onclick="show(${i})" id="tab-${i}">
738
  <span class="tab-emoji">${p.emoji}</span>
739
  <span class="tab-name">${p.name}</span>
740
+ ${dot}
741
  </button>`;
742
+ }).join("");
 
743
 
744
  // Build detail panels
745
+ const panels = platforms.map((p, i) => {
746
+ const allSet = p.envVars.every((v) => v.set);
747
+ const anySet = p.envVars.some((v) => v.set);
748
+
749
+ const stepsList = p.steps.map((s, si) =>
750
+ `<div class="step">
751
+ <div class="step-num">${si + 1}</div>
752
+ <div>
753
+ <div class="step-title">${s.title}</div>
754
+ <div class="step-body">${s.body}</div>
755
+ </div>
756
+ </div>`
757
+ ).join("");
758
+
759
+ const envRows = p.envVars.map((v) =>
760
+ `<div class="env-row">
761
  <div class="env-info">
762
  <code class="env-name">${v.name}</code>
763
  <span class="env-desc">${v.desc}</span>
764
  </div>
765
  <div class="env-actions">
766
+ ${v.set
767
+ ? `<span class="badge ok">SET</span>`
768
+ : `<span class="badge off">NOT SET</span>`}
769
+ <button class="btn-copy" onclick="copy('${v.name}',this)">Copy name</button>
770
  </div>
771
+ </div>`
772
+ ).join("");
773
+
774
+ const banner = allSet
775
+ ? `<div class="banner banner-ok">All credentials configured — restart Space if you just added them.</div>`
776
+ : anySet
777
+ ? `<div class="banner banner-warn">Partially configured — add the missing env vars below.</div>`
778
+ : `<div class="banner banner-info">Not yet configured — follow the steps below.</div>`;
779
+
780
+ return `<div class="panel${i === 0 ? " active" : ""}" id="panel-${i}">
781
+ <div class="panel-head">
 
782
  <span class="panel-emoji">${p.emoji}</span>
783
  <div>
784
+ <div class="panel-title">${p.name}</div>
785
+ <div class="panel-links">
786
+ <a href="${p.setupUrl}" target="_blank" rel="noopener">Open Developer Portal →</a>
787
+ ${p.docsUrl ? `<a href="${p.docsUrl}" target="_blank" rel="noopener">Official Docs →</a>` : ""}
788
+ </div>
789
  </div>
790
  </div>
791
 
792
+ ${banner}
793
 
794
+ <div class="section-label">Setup Steps</div>
795
  <div class="steps-list">${stepsList}</div>
796
 
797
+ <div class="section-label">Callback URL
798
+ <span class="section-hint">Paste this in the developer portal when asked for "Redirect URI" or "Callback URL"</span>
799
+ </div>
800
  <div class="copy-block">
801
+ <span class="copy-block-text">${p.callbackUrl}</span>
802
+ <button class="btn-copy btn-copy-accent" onclick="copy('${p.callbackUrl}',this)">Copy</button>
803
  </div>
 
804
 
805
+ <div class="section-label">Space Secrets to Add
806
+ <span class="section-hint">HF Space → Settings → Variables &amp; Secrets</span>
 
 
 
807
  </div>
808
+ <div class="env-list">${envRows}</div>
809
+
810
+ <a href="${settingsUrl}" target="_blank" rel="noopener" class="settings-btn">
811
+ Open Space Settings → Variables &amp; Secrets
812
+ </a>
813
+ <p class="hint-final">After adding all secrets, click <strong>Restart Space</strong> for them to take effect.</p>
814
  </div>`;
815
+ }).join("");
 
816
 
817
  return `<!DOCTYPE html>
818
  <html lang="en">
819
  <head>
820
  <meta charset="UTF-8">
821
+ <meta name="viewport" content="width=device-width,initial-scale=1">
822
  <title>Platform Setup — HuggingPost</title>
 
823
  <style>
824
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
825
+ :root {
826
+ color-scheme: dark;
827
+ --bg: #08080f;
828
+ --panel: #12111b;
829
+ --panel2: #151421;
830
+ --line: #26243a;
831
+ --text: #f6f4ff;
832
+ --muted: #7f7a9e;
833
+ --soft: #b8b3d7;
834
+ --good: #22c55e;
835
+ --warn: #f5c542;
836
+ --bad: #fb7185;
837
+ --accent: #3b82f6;
838
+ --accent2:#8b5cf6;
839
+ }
840
+ body {
841
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
842
+ background: var(--bg);
843
+ color: var(--text);
844
+ font-size: 13px;
845
+ height: 100dvh;
846
+ display: flex;
847
+ flex-direction: column;
848
+ overflow: hidden;
849
+ }
850
+
851
+ /* ── Top bar ─────────────────────────────────────────── */
852
+ .topbar {
853
+ display: flex;
854
+ align-items: center;
855
+ gap: 16px;
856
+ padding: 0 20px;
857
+ height: 48px;
858
+ border-bottom: 1px solid var(--line);
859
+ background: var(--panel);
860
+ flex-shrink: 0;
861
+ }
862
+ .topbar-back {
863
+ color: var(--muted);
864
+ text-decoration: none;
865
+ font-size: .78rem;
866
+ font-weight: 700;
867
+ letter-spacing: .04em;
868
+ display: flex;
869
+ align-items: center;
870
+ gap: 6px;
871
+ transition: color .15s;
872
+ }
873
+ .topbar-back:hover { color: var(--text); }
874
+ .topbar-title {
875
+ font-size: .78rem;
876
+ font-weight: 850;
877
+ letter-spacing: .12em;
878
+ text-transform: uppercase;
879
+ color: var(--soft);
880
+ }
881
+ .topbar-count {
882
+ margin-left: auto;
883
+ font-size: .72rem;
884
+ font-weight: 850;
885
+ color: var(--muted);
886
+ letter-spacing: .08em;
887
+ text-transform: uppercase;
888
+ border: 1px solid var(--line);
889
+ border-radius: 999px;
890
+ padding: 3px 10px;
891
+ }
892
+
893
+ /* ── Layout ──────────────────────────────────────────── */
894
+ .layout { display: flex; flex: 1; overflow: hidden; }
895
+
896
+ /* ── Sidebar ─────────────────────────────────────────── */
897
+ .sidebar {
898
+ width: 210px;
899
+ flex-shrink: 0;
900
+ background: var(--panel);
901
+ border-right: 1px solid var(--line);
902
+ overflow-y: auto;
903
+ padding: 10px 8px;
904
+ display: flex;
905
+ flex-direction: column;
906
+ gap: 2px;
907
+ }
908
+ .sidebar-label {
909
+ font-size: .62rem;
910
+ font-weight: 850;
911
+ letter-spacing: .14em;
912
+ text-transform: uppercase;
913
+ color: var(--muted);
914
+ padding: 6px 10px 10px;
915
+ }
916
+ .plat-tab {
917
+ width: 100%;
918
+ background: none;
919
+ border: none;
920
+ color: var(--soft);
921
+ font: inherit;
922
+ font-size: .82rem;
923
+ font-weight: 600;
924
+ display: flex;
925
+ align-items: center;
926
+ gap: 9px;
927
+ padding: 8px 10px;
928
+ border-radius: 8px;
929
+ cursor: pointer;
930
+ text-align: left;
931
+ transition: background .12s, color .12s;
932
+ border: 1px solid transparent;
933
+ }
934
+ .plat-tab:hover { background: var(--panel2); color: var(--text); }
935
+ .plat-tab.active {
936
+ background: var(--panel2);
937
+ color: var(--text);
938
+ border-color: var(--line);
939
+ }
940
+ .tab-emoji { font-size: .95rem; width: 20px; text-align: center; flex-shrink: 0; }
941
+ .tab-name { flex: 1; }
942
+ .dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
943
+ .dot-ok { background: var(--good); }
944
+ .dot-warn { background: var(--warn); }
945
+ .dot-off { background: var(--line); }
946
+
947
+ /* ── Main panel ──────────────────────────────────────── */
948
+ .main { flex: 1; overflow-y: auto; padding: 28px 32px; max-width: 780px; }
949
+ .panel { display: none; animation: fadein .18s ease; }
950
+ .panel.active { display: block; }
951
+ @keyframes fadein { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:none; } }
952
+
953
+ .panel-head {
954
+ display: flex;
955
+ align-items: flex-start;
956
+ gap: 14px;
957
+ margin-bottom: 18px;
958
+ }
959
+ .panel-emoji { font-size: 2.2rem; flex-shrink: 0; line-height: 1; }
960
+ .panel-title { font-size: 1.4rem; font-weight: 850; margin-bottom: 6px; }
961
+ .panel-links { display: flex; gap: 14px; flex-wrap: wrap; }
962
+ .panel-links a {
963
+ color: var(--accent2);
964
+ font-size: .76rem;
965
+ font-weight: 700;
966
+ text-decoration: none;
967
+ letter-spacing: .02em;
968
+ }
969
+ .panel-links a:hover { text-decoration: underline; }
970
+
971
+ /* Banner */
972
+ .banner {
973
+ padding: 10px 14px;
974
+ border-radius: 8px;
975
+ font-size: .8rem;
976
+ font-weight: 600;
977
+ margin-bottom: 20px;
978
+ border: 1px solid transparent;
979
+ }
980
+ .banner-ok { background: rgba(34,197,94,.08); border-color: rgba(34,197,94,.22); color: #86efac; }
981
+ .banner-warn { background: rgba(245,197,66,.08); border-color: rgba(245,197,66,.22); color: #fde68a; }
982
+ .banner-info { background: rgba(59,130,246,.08); border-color: rgba(59,130,246,.22); color: #93c5fd; }
983
+
984
  /* Section labels */
985
+ .section-label {
986
+ font-size: .62rem;
987
+ font-weight: 850;
988
+ letter-spacing: .14em;
989
+ text-transform: uppercase;
990
+ color: var(--muted);
991
+ margin: 22px 0 10px;
992
+ display: flex;
993
+ align-items: baseline;
994
+ gap: 10px;
995
+ }
996
+ .section-hint {
997
+ font-size: .7rem;
998
+ font-weight: 500;
999
+ letter-spacing: 0;
1000
+ text-transform: none;
1001
+ color: var(--muted);
1002
+ opacity: .7;
1003
+ }
1004
+
1005
  /* Steps */
1006
+ .steps-list { display: flex; flex-direction: column; gap: 4px; }
1007
+ .step {
1008
+ display: flex;
1009
+ gap: 12px;
1010
+ padding: 11px 14px;
1011
+ border-radius: 8px;
1012
+ background: var(--panel);
1013
+ border: 1px solid var(--line);
1014
+ }
1015
+ .step-num {
1016
+ width: 22px;
1017
+ height: 22px;
1018
+ border-radius: 50%;
1019
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
1020
+ color: #fff;
1021
+ font-size: .68rem;
1022
+ font-weight: 850;
1023
+ display: flex;
1024
+ align-items: center;
1025
+ justify-content: center;
1026
+ flex-shrink: 0;
1027
+ margin-top: 1px;
1028
+ }
1029
+ .step-title { font-size: .82rem; font-weight: 800; margin-bottom: 3px; color: var(--text); }
1030
+ .step-body { font-size: .78rem; color: var(--soft); line-height: 1.6; }
1031
+ .step-body strong { color: var(--text); font-weight: 800; }
1032
+ .step-body code {
1033
+ background: var(--panel2);
1034
+ border: 1px solid var(--line);
1035
+ padding: 1px 5px;
1036
+ border-radius: 4px;
1037
+ font-size: .85em;
1038
+ color: var(--text);
1039
+ }
1040
+ .step-body a { color: var(--accent2); text-decoration: none; }
1041
+ .step-body a:hover { text-decoration: underline; }
1042
+
1043
+ /* Callback copy block */
1044
+ .copy-block {
1045
+ display: flex;
1046
+ align-items: center;
1047
+ gap: 10px;
1048
+ background: var(--panel2);
1049
+ border: 1px solid var(--line);
1050
+ border-radius: 8px;
1051
+ padding: 11px 14px;
1052
+ }
1053
+ .copy-block-text {
1054
+ flex: 1;
1055
+ font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, monospace;
1056
+ font-size: .78rem;
1057
+ color: var(--accent2);
1058
+ word-break: break-all;
1059
+ opacity: .9;
1060
+ }
1061
+
1062
+ /* Env var rows */
1063
+ .env-list { display: flex; flex-direction: column; gap: 5px; }
1064
+ .env-row {
1065
+ display: flex;
1066
+ align-items: center;
1067
+ gap: 10px;
1068
+ padding: 10px 14px;
1069
+ background: var(--panel);
1070
+ border: 1px solid var(--line);
1071
+ border-radius: 8px;
1072
+ }
1073
+ .env-info { flex: 1; display: flex; flex-direction: column; gap: 3px; }
1074
+ .env-name {
1075
+ font-family: ui-monospace, "Cascadia Code", monospace;
1076
+ font-size: .78rem;
1077
+ color: var(--accent2);
1078
+ background: rgba(139,92,246,.12);
1079
+ border: 1px solid rgba(139,92,246,.2);
1080
+ padding: 2px 7px;
1081
+ border-radius: 5px;
1082
+ width: fit-content;
1083
+ }
1084
+ .env-desc { font-size: .72rem; color: var(--muted); }
1085
+ .env-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
1086
+
1087
  /* Badges */
1088
+ .badge {
1089
+ display: inline-flex;
1090
+ align-items: center;
1091
+ border: 1px solid var(--line);
1092
+ border-radius: 999px;
1093
+ padding: 2px 8px;
1094
+ font-size: .66rem;
1095
+ font-weight: 850;
1096
+ letter-spacing: .06em;
1097
+ text-transform: uppercase;
1098
+ white-space: nowrap;
1099
+ }
1100
+ .badge.ok { color: var(--good); border-color: rgba(34,197,94,.3); background: rgba(34,197,94,.1); }
1101
+ .badge.off { color: var(--bad); border-color: rgba(251,113,133,.3); background: rgba(251,113,133,.1); }
1102
+
1103
+ /* Buttons */
1104
+ .btn-copy {
1105
+ background: var(--panel2);
1106
+ border: 1px solid var(--line);
1107
+ color: var(--soft);
1108
+ font: inherit;
1109
+ font-size: .72rem;
1110
+ font-weight: 700;
1111
+ padding: 5px 10px;
1112
+ border-radius: 6px;
1113
+ cursor: pointer;
1114
+ transition: background .12s, color .12s;
1115
+ flex-shrink: 0;
1116
+ white-space: nowrap;
1117
+ }
1118
+ .btn-copy:hover { background: var(--line); color: var(--text); }
1119
+ .btn-copy.copied { background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.3); color: var(--good); }
1120
+ .btn-copy-accent {
1121
+ background: rgba(59,130,246,.12);
1122
+ border-color: rgba(59,130,246,.3);
1123
+ color: #93c5fd;
1124
+ font-size: .76rem;
1125
+ padding: 6px 14px;
1126
+ }
1127
+ .btn-copy-accent:hover { background: rgba(59,130,246,.2); }
1128
+
1129
+ /* Settings CTA */
1130
+ .settings-btn {
1131
+ display: inline-flex;
1132
+ align-items: center;
1133
+ margin-top: 16px;
1134
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
1135
+ color: #fff;
1136
+ text-decoration: none;
1137
+ padding: 10px 20px;
1138
+ border-radius: 8px;
1139
+ font-size: .82rem;
1140
+ font-weight: 850;
1141
+ transition: opacity .15s;
1142
+ }
1143
+ .settings-btn:hover { opacity: .85; }
1144
+ .hint-final {
1145
+ margin-top: 10px;
1146
+ font-size: .74rem;
1147
+ color: var(--muted);
1148
+ line-height: 1.5;
1149
+ padding-bottom: 32px;
1150
+ }
1151
+ .hint-final strong { color: var(--soft); }
1152
+
1153
  /* Mobile */
1154
+ @media (max-width: 680px) {
1155
+ body { overflow: auto; height: auto; }
1156
+ .layout { flex-direction: column; overflow: visible; }
1157
+ .sidebar {
1158
+ width: 100%;
1159
+ border-right: none;
1160
+ border-bottom: 1px solid var(--line);
1161
+ flex-direction: row;
1162
+ flex-wrap: wrap;
1163
+ overflow-x: auto;
1164
+ padding: 8px;
1165
+ gap: 4px;
1166
+ }
1167
+ .sidebar-label { display: none; }
1168
+ .plat-tab { width: auto; flex: 0 0 auto; }
1169
+ .tab-name { display: none; }
1170
+ .main { padding: 16px; }
1171
  }
1172
  </style>
1173
  </head>
1174
  <body>
1175
  <div class="topbar">
1176
+ <a class="topbar-back" href="/">← Dashboard</a>
1177
+ <span class="topbar-title">Platform Setup Guide</span>
1178
+ <span class="topbar-count">${configuredCount} / ${platforms.length} configured</span>
1179
  </div>
1180
  <div class="layout">
1181
  <nav class="sidebar">
 
1189
  <script>
1190
  const PLATFORM_IDS = ${JSON.stringify(platforms.map((p) => p.id))};
1191
  function show(i) {
1192
+ document.querySelectorAll('.plat-tab').forEach((t,j)=>t.classList.toggle('active',j===i));
1193
+ document.querySelectorAll('.panel').forEach((p,j)=>p.classList.toggle('active',j===i));
1194
+ if(PLATFORM_IDS[i]) history.replaceState(null,'','#'+PLATFORM_IDS[i]);
1195
  }
1196
+ function copy(text,btn) {
1197
+ const finish = () => {
1198
  const orig = btn.textContent;
1199
  btn.textContent = 'Copied!';
1200
  btn.classList.add('copied');
1201
+ setTimeout(()=>{ btn.textContent=orig; btn.classList.remove('copied'); },1800);
1202
+ };
1203
+ if(navigator.clipboard) { navigator.clipboard.writeText(text).then(finish).catch(fallback); }
1204
+ else fallback();
1205
+ function fallback() {
1206
+ const ta=document.createElement('textarea');
1207
+ ta.value=text; ta.style.cssText='position:fixed;opacity:0';
1208
  document.body.appendChild(ta); ta.select();
1209
+ try{document.execCommand('copy');}catch{}
1210
+ document.body.removeChild(ta); finish();
 
 
 
 
 
 
 
 
 
 
 
 
1211
  }
1212
+ }
1213
+ (function(){
1214
+ const hash=location.hash.replace('#','').toLowerCase();
1215
+ if(hash){const idx=PLATFORM_IDS.indexOf(hash);if(idx!==-1)show(idx);}
1216
  })();
1217
  </script>
1218
  </body>