somratpro Claude Sonnet 4.6 commited on
Commit
f4e74f4
Β·
1 Parent(s): 248182d

add direct-connect guides + restyle setup page

Browse files

- Add getDirectPlatformDetails(): guides for Bluesky, Mastodon,
Telegram, Dev.to, Hashnode, Nostr, Lemmy, Warpcast β€” each with
step-by-step instructions and field descriptions
- Sidebar split into "OAuth Platforms" + "Direct Connect" sections
- Remove all gradients: step-num and CTA use solid colors
- Single accent color (#3b82f6, drop --accent2)
- Primary CTA buttons: white background, dark text
- Copy URL button: white with dark text (btn-copy-white)
- Panel links: accent blue (was purple)
- Env-name pill: blue tint (was purple)

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

Files changed (1) hide show
  1. health-server.js +270 -35
health-server.js CHANGED
@@ -213,6 +213,150 @@ function getSocialPlatforms() {
213
  ];
214
  }
215
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  // Returns detailed per-platform OAuth setup guide data.
217
  // publicUrl: "https://somratpro-huggingpost.hf.space" (no trailing slash)
218
  function getOAuthPlatformDetails(publicUrl) {
@@ -708,17 +852,20 @@ function renderSetupPage() {
708
  const publicUrl = spaceHost
709
  ? `https://${spaceHost}`
710
  : "http://localhost:7860";
 
711
  const settingsUrl = spaceId
712
  ? `https://huggingface.co/spaces/${spaceId}/settings`
713
  : "https://huggingface.co/settings/spaces";
714
 
715
  const platforms = getOAuthPlatformDetails(publicUrl);
 
716
  const configuredCount = platforms.filter((p) =>
717
  p.envVars.every((v) => v.set),
718
  ).length;
 
719
 
720
- // Build sidebar items
721
- const sidebarItems = platforms.map((p, i) => {
722
  const allSet = p.envVars.every((v) => v.set);
723
  const anySet = p.envVars.some((v) => v.set);
724
  const dot = allSet
@@ -733,8 +880,17 @@ function renderSetupPage() {
733
  </button>`;
734
  }).join("");
735
 
736
- // Build detail panels
737
- const panels = platforms.map((p, i) => {
 
 
 
 
 
 
 
 
 
738
  const allSet = p.envVars.every((v) => v.set);
739
  const anySet = p.envVars.some((v) => v.set);
740
 
@@ -791,7 +947,7 @@ function renderSetupPage() {
791
  </div>
792
  <div class="copy-block">
793
  <span class="copy-block-text">${p.callbackUrl}</span>
794
- <button class="btn-copy btn-copy-accent" onclick="copy('${p.callbackUrl}',this)">Copy</button>
795
  </div>
796
 
797
  <div class="section-label">Space Secrets to Add
@@ -799,13 +955,70 @@ function renderSetupPage() {
799
  </div>
800
  <div class="env-list">${envRows}</div>
801
 
802
- <a href="${settingsUrl}" target="_blank" rel="noopener" class="settings-btn">
803
  Open Space Settings β†’ Variables &amp; Secrets
804
  </a>
805
  <p class="hint-final">After adding all secrets, click <strong>Restart Space</strong> for them to take effect.</p>
806
  </div>`;
807
  }).join("");
808
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
809
  return `<!DOCTYPE html>
810
  <html lang="en">
811
  <head>
@@ -827,7 +1040,6 @@ function renderSetupPage() {
827
  --warn: #f5c542;
828
  --bad: #fb7185;
829
  --accent: #3b82f6;
830
- --accent2:#8b5cf6;
831
  }
832
  body {
833
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
@@ -905,10 +1117,15 @@ body {
905
  color: var(--muted);
906
  padding: 6px 10px 10px;
907
  }
 
 
 
 
 
908
  .plat-tab {
909
  width: 100%;
910
  background: none;
911
- border: none;
912
  color: var(--soft);
913
  font: inherit;
914
  font-size: .82rem;
@@ -921,7 +1138,6 @@ body {
921
  cursor: pointer;
922
  text-align: left;
923
  transition: background .12s, color .12s;
924
- border: 1px solid transparent;
925
  }
926
  .plat-tab:hover { background: var(--panel2); color: var(--text); }
927
  .plat-tab.active {
@@ -952,7 +1168,7 @@ body {
952
  .panel-title { font-size: 1.4rem; font-weight: 850; margin-bottom: 6px; }
953
  .panel-links { display: flex; gap: 14px; flex-wrap: wrap; }
954
  .panel-links a {
955
- color: var(--accent2);
956
  font-size: .76rem;
957
  font-weight: 700;
958
  text-decoration: none;
@@ -1008,7 +1224,7 @@ body {
1008
  width: 22px;
1009
  height: 22px;
1010
  border-radius: 50%;
1011
- background: linear-gradient(135deg, var(--accent), var(--accent2));
1012
  color: #fff;
1013
  font-size: .68rem;
1014
  font-weight: 850;
@@ -1021,6 +1237,7 @@ body {
1021
  .step-title { font-size: .82rem; font-weight: 800; margin-bottom: 3px; color: var(--text); }
1022
  .step-body { font-size: .78rem; color: var(--soft); line-height: 1.6; }
1023
  .step-body strong { color: var(--text); font-weight: 800; }
 
1024
  .step-body code {
1025
  background: var(--panel2);
1026
  border: 1px solid var(--line);
@@ -1029,7 +1246,7 @@ body {
1029
  font-size: .85em;
1030
  color: var(--text);
1031
  }
1032
- .step-body a { color: var(--accent2); text-decoration: none; }
1033
  .step-body a:hover { text-decoration: underline; }
1034
 
1035
  /* Callback copy block */
@@ -1046,12 +1263,12 @@ body {
1046
  flex: 1;
1047
  font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, monospace;
1048
  font-size: .78rem;
1049
- color: var(--accent2);
1050
  word-break: break-all;
1051
  opacity: .9;
1052
  }
1053
 
1054
- /* Env var rows */
1055
  .env-list { display: flex; flex-direction: column; gap: 5px; }
1056
  .env-row {
1057
  display: flex;
@@ -1066,9 +1283,9 @@ body {
1066
  .env-name {
1067
  font-family: ui-monospace, "Cascadia Code", monospace;
1068
  font-size: .78rem;
1069
- color: var(--accent2);
1070
- background: rgba(139,92,246,.12);
1071
- border: 1px solid rgba(139,92,246,.2);
1072
  padding: 2px 7px;
1073
  border-radius: 5px;
1074
  width: fit-content;
@@ -1092,7 +1309,7 @@ body {
1092
  .badge.ok { color: var(--good); border-color: rgba(34,197,94,.3); background: rgba(34,197,94,.1); }
1093
  .badge.off { color: var(--bad); border-color: rgba(251,113,133,.3); background: rgba(251,113,133,.1); }
1094
 
1095
- /* Buttons */
1096
  .btn-copy {
1097
  background: var(--panel2);
1098
  border: 1px solid var(--line);
@@ -1109,30 +1326,43 @@ body {
1109
  }
1110
  .btn-copy:hover { background: var(--line); color: var(--text); }
1111
  .btn-copy.copied { background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.3); color: var(--good); }
1112
- .btn-copy-accent {
1113
- background: rgba(59,130,246,.12);
1114
- border-color: rgba(59,130,246,.3);
1115
- color: #93c5fd;
 
 
 
1116
  font-size: .76rem;
 
1117
  padding: 6px 14px;
 
 
 
 
 
1118
  }
1119
- .btn-copy-accent:hover { background: rgba(59,130,246,.2); }
 
1120
 
1121
- /* Settings CTA */
1122
- .settings-btn {
1123
  display: inline-flex;
1124
  align-items: center;
1125
- margin-top: 16px;
1126
- background: linear-gradient(135deg, var(--accent), var(--accent2));
1127
- color: #fff;
1128
  text-decoration: none;
1129
- padding: 10px 20px;
1130
  border-radius: 8px;
1131
  font-size: .82rem;
1132
  font-weight: 850;
1133
- transition: opacity .15s;
 
 
1134
  }
1135
- .settings-btn:hover { opacity: .85; }
 
1136
  .hint-final {
1137
  margin-top: 10px;
1138
  font-size: .74rem;
@@ -1157,6 +1387,7 @@ body {
1157
  gap: 4px;
1158
  }
1159
  .sidebar-label { display: none; }
 
1160
  .plat-tab { width: auto; flex: 0 0 auto; }
1161
  .tab-name { display: none; }
1162
  .main { padding: 16px; }
@@ -1167,19 +1398,23 @@ body {
1167
  <div class="topbar">
1168
  <a class="topbar-back" href="/">← Dashboard</a>
1169
  <span class="topbar-title">Platform Setup Guide</span>
1170
- <span class="topbar-count">${configuredCount} / ${platforms.length} configured</span>
1171
  </div>
1172
  <div class="layout">
1173
  <nav class="sidebar">
1174
  <div class="sidebar-label">OAuth Platforms</div>
1175
- ${sidebarItems}
 
 
 
1176
  </nav>
1177
  <main class="main">
1178
- ${panels}
 
1179
  </main>
1180
  </div>
1181
  <script>
1182
- const PLATFORM_IDS = ${JSON.stringify(platforms.map((p) => p.id))};
1183
  function show(i) {
1184
  document.querySelectorAll('.plat-tab').forEach((t,j)=>t.classList.toggle('active',j===i));
1185
  document.querySelectorAll('.panel').forEach((p,j)=>p.classList.toggle('active',j===i));
 
213
  ];
214
  }
215
 
216
+ // Returns detailed per-platform "direct connect" guide data (no OAuth / no env vars).
217
+ // These platforms connect entirely inside the Postiz UI β€” no developer portal needed.
218
+ function getDirectPlatformDetails(postizUrl) {
219
+ const postizIntegrations = `${postizUrl}/integrations`;
220
+ return [
221
+ {
222
+ id: "bluesky",
223
+ name: "Bluesky",
224
+ emoji: "πŸ¦‹",
225
+ docsUrl: "https://bsky.social",
226
+ postizUrl: postizIntegrations,
227
+ fields: [
228
+ { label: "Service", value: "https://bsky.social", hint: "Default for bsky.social users. Custom instance: use your instance URL." },
229
+ { label: "Identifier", value: "yourname.bsky.social", hint: "Your full Bluesky handle." },
230
+ { label: "Password", value: "App Password", hint: "NOT your login password β€” generate a dedicated App Password in Bluesky settings." },
231
+ ],
232
+ steps: [
233
+ { title: "Create a Bluesky account", body: "Sign up at <a href=\"https://bsky.app\" target=\"_blank\" rel=\"noopener\">bsky.app</a> if you don't have one. Custom PDS users: use your own instance URL." },
234
+ { title: "Generate an App Password", body: "In Bluesky β†’ <strong>Settings β†’ Privacy and Security β†’ App Passwords β†’ Add App Password</strong>. Name it <em>HuggingPost</em>. Copy the generated password β€” it's shown only once." },
235
+ { title: "Open Postiz Integrations", body: "Click the button below to go to Postiz β†’ click <strong>Connect</strong> next to Bluesky." },
236
+ { title: "Fill in credentials", body: "<strong>Service:</strong> <code>https://bsky.social</code> (or your instance URL)<br><strong>Identifier:</strong> your full handle (e.g. <code>yourname.bsky.social</code>)<br><strong>Password:</strong> the App Password from Step 2 β€” <em>not</em> your login password." },
237
+ { title: "Click Connect", body: "Postiz authenticates and adds your Bluesky account. You can post immediately." },
238
+ ],
239
+ },
240
+ {
241
+ id: "mastodon",
242
+ name: "Mastodon",
243
+ emoji: "🐘",
244
+ docsUrl: "https://joinmastodon.org",
245
+ postizUrl: postizIntegrations,
246
+ fields: [
247
+ { label: "Service", value: "https://mastodon.social", hint: "Your Mastodon instance URL (e.g. https://fosstodon.org)." },
248
+ { label: "Identifier", value: "yourhandle", hint: "Your username without the @instance part." },
249
+ { label: "Password", value: "Your password", hint: "Your Mastodon account password." },
250
+ ],
251
+ steps: [
252
+ { title: "Find your Mastodon instance URL", body: "Check your profile URL β€” it's the domain part (e.g. <code>mastodon.social</code>, <code>fosstodon.org</code>)." },
253
+ { title: "Open Postiz Integrations", body: "Click the button below β†’ click <strong>Connect</strong> next to Mastodon." },
254
+ { title: "Fill in credentials", body: "<strong>Service:</strong> your full instance URL with https (e.g. <code>https://mastodon.social</code>)<br><strong>Identifier:</strong> your username (the part before @instance)<br><strong>Password:</strong> your account password." },
255
+ { title: "Click Connect", body: "Postiz will authenticate via Mastodon's API and add your account." },
256
+ ],
257
+ },
258
+ {
259
+ id: "telegram",
260
+ name: "Telegram",
261
+ emoji: "✈️",
262
+ docsUrl: "https://core.telegram.org/bots#how-do-i-create-a-bot",
263
+ postizUrl: postizIntegrations,
264
+ fields: [
265
+ { label: "Bot Token", value: "123456:ABC-DEF...", hint: "From @BotFather. Format: number:string." },
266
+ ],
267
+ steps: [
268
+ { title: "Create a Telegram Bot", body: "Open Telegram β†’ message <strong>@BotFather</strong> β†’ send <code>/newbot</code> β†’ follow prompts β†’ copy the <strong>Bot Token</strong> (format: <code>123456789:ABC-...</code>)." },
269
+ { title: "(Optional) Add bot to a channel", body: "To post to a Telegram channel: add your bot as an <strong>Admin</strong> of the channel with post permission." },
270
+ { title: "Open Postiz Integrations", body: "Click the button below β†’ click <strong>Connect</strong> next to Telegram." },
271
+ { title: "Enter Bot Token", body: "Paste your Bot Token from @BotFather. Click Connect." },
272
+ ],
273
+ },
274
+ {
275
+ id: "devto",
276
+ name: "Dev.to",
277
+ emoji: "πŸ’»",
278
+ docsUrl: "https://dev.to/settings/extensions",
279
+ postizUrl: postizIntegrations,
280
+ fields: [
281
+ { label: "API Key", value: "Your Dev.to API key", hint: "From dev.to β†’ Settings β†’ Extensions β†’ DEV API Keys." },
282
+ ],
283
+ steps: [
284
+ { title: "Log in to Dev.to", body: "Go to <a href=\"https://dev.to\" target=\"_blank\" rel=\"noopener\">dev.to</a> and sign in." },
285
+ { title: "Generate an API key", body: "Go to <strong>Settings β†’ Extensions β†’ DEV API Keys</strong>. Enter a description (e.g. <em>HuggingPost</em>) and click <strong>Generate API Key</strong>. Copy the key." },
286
+ { title: "Open Postiz Integrations", body: "Click the button below β†’ click <strong>Connect</strong> next to Dev.to." },
287
+ { title: "Enter API key", body: "Paste your API key. Click Connect." },
288
+ ],
289
+ },
290
+ {
291
+ id: "hashnode",
292
+ name: "Hashnode",
293
+ emoji: "πŸ“°",
294
+ docsUrl: "https://hashnode.com/settings/developer",
295
+ postizUrl: postizIntegrations,
296
+ fields: [
297
+ { label: "Personal Access Token", value: "Your Hashnode token", hint: "From hashnode.com β†’ Account Settings β†’ Developer β†’ Personal Access Token." },
298
+ ],
299
+ steps: [
300
+ { title: "Log in to Hashnode", body: "Go to <a href=\"https://hashnode.com\" target=\"_blank\" rel=\"noopener\">hashnode.com</a> and sign in." },
301
+ { title: "Generate a Personal Access Token", body: "Go to <strong>Account Settings β†’ Developer β†’ Personal Access Token β†’ Generate new token</strong>. Name it <em>HuggingPost</em>. Copy the token." },
302
+ { title: "Open Postiz Integrations", body: "Click the button below β†’ click <strong>Connect</strong> next to Hashnode." },
303
+ { title: "Enter token", body: "Paste your Personal Access Token. Click Connect." },
304
+ ],
305
+ },
306
+ {
307
+ id: "nostr",
308
+ name: "Nostr",
309
+ emoji: "πŸ”‘",
310
+ docsUrl: "https://nostr.how/en/get-started",
311
+ postizUrl: postizIntegrations,
312
+ fields: [
313
+ { label: "Private Key", value: "nsec1...", hint: "Your Nostr private key in nsec (bech32) format." },
314
+ ],
315
+ steps: [
316
+ { title: "Get a Nostr keypair", body: "Use a Nostr client like <a href=\"https://snort.social\" target=\"_blank\" rel=\"noopener\">Snort</a> or <a href=\"https://primal.net\" target=\"_blank\" rel=\"noopener\">Primal</a> to create or export your keys. You need the <strong>private key</strong> in <code>nsec1...</code> format." },
317
+ { title: "⚠️ Security note", body: "Your private key controls your entire Nostr identity. Only enter it in trusted apps. HuggingPost is self-hosted β€” your key stays in your container." },
318
+ { title: "Open Postiz Integrations", body: "Click the button below β†’ click <strong>Connect</strong> next to Nostr." },
319
+ { title: "Enter private key", body: "Paste your <code>nsec1...</code> private key. Click Connect." },
320
+ ],
321
+ },
322
+ {
323
+ id: "lemmy",
324
+ name: "Lemmy",
325
+ emoji: "🐾",
326
+ docsUrl: "https://join-lemmy.org",
327
+ postizUrl: postizIntegrations,
328
+ fields: [
329
+ { label: "Instance", value: "https://lemmy.world", hint: "Your Lemmy instance URL." },
330
+ { label: "Username", value: "yourhandle", hint: "Your Lemmy username." },
331
+ { label: "Password", value: "Your password", hint: "Your Lemmy account password." },
332
+ ],
333
+ steps: [
334
+ { title: "Find your Lemmy instance", body: "You need the full URL of your Lemmy instance (e.g. <code>https://lemmy.world</code>, <code>https://lemmy.ml</code>). Find it at <a href=\"https://join-lemmy.org\" target=\"_blank\" rel=\"noopener\">join-lemmy.org</a>." },
335
+ { title: "Open Postiz Integrations", body: "Click the button below β†’ click <strong>Connect</strong> next to Lemmy." },
336
+ { title: "Fill in credentials", body: "<strong>Instance:</strong> your instance URL<br><strong>Username:</strong> your account username<br><strong>Password:</strong> your account password." },
337
+ { title: "Click Connect", body: "Postiz authenticates via Lemmy's API." },
338
+ ],
339
+ },
340
+ {
341
+ id: "warpcast",
342
+ name: "Warpcast",
343
+ emoji: "🟣",
344
+ docsUrl: "https://docs.farcaster.xyz",
345
+ postizUrl: postizIntegrations,
346
+ fields: [
347
+ { label: "FID", value: "12345", hint: "Your Farcaster user ID (numeric)." },
348
+ { label: "Private Key", value: "0x...", hint: "Your Farcaster custody private key (hex, 0x-prefixed)." },
349
+ ],
350
+ steps: [
351
+ { title: "Find your FID", body: "Your Farcaster ID (FID) is a numeric value. In Warpcast β†’ Profile β†’ the number in your profile URL, or use <a href=\"https://www.farcaster.xyz\" target=\"_blank\" rel=\"noopener\">farcaster.xyz</a> to look it up." },
352
+ { title: "Export your private key", body: "In Warpcast β†’ <strong>Settings β†’ Advanced β†’ Recovery phrase</strong>, or use a Farcaster tool to derive your custody private key. The key is in hex format (<code>0x...</code>)." },
353
+ { title: "Open Postiz Integrations", body: "Click the button below β†’ click <strong>Connect</strong> next to Warpcast." },
354
+ { title: "Enter FID and private key", body: "Paste your FID and private key. Click Connect." },
355
+ ],
356
+ },
357
+ ];
358
+ }
359
+
360
  // Returns detailed per-platform OAuth setup guide data.
361
  // publicUrl: "https://somratpro-huggingpost.hf.space" (no trailing slash)
362
  function getOAuthPlatformDetails(publicUrl) {
 
852
  const publicUrl = spaceHost
853
  ? `https://${spaceHost}`
854
  : "http://localhost:7860";
855
+ const postizUrl = publicUrl + "/app";
856
  const settingsUrl = spaceId
857
  ? `https://huggingface.co/spaces/${spaceId}/settings`
858
  : "https://huggingface.co/settings/spaces";
859
 
860
  const platforms = getOAuthPlatformDetails(publicUrl);
861
+ const directPlatforms = getDirectPlatformDetails(postizUrl);
862
  const configuredCount = platforms.filter((p) =>
863
  p.envVars.every((v) => v.set),
864
  ).length;
865
+ const totalPanels = platforms.length + directPlatforms.length;
866
 
867
+ // ── Build sidebar ────────────────────────────────────────────────────────────
868
+ const oauthSidebarItems = platforms.map((p, i) => {
869
  const allSet = p.envVars.every((v) => v.set);
870
  const anySet = p.envVars.some((v) => v.set);
871
  const dot = allSet
 
880
  </button>`;
881
  }).join("");
882
 
883
+ const directSidebarItems = directPlatforms.map((p, i) => {
884
+ const idx = platforms.length + i;
885
+ return `<button class="plat-tab" onclick="show(${idx})" id="tab-${idx}">
886
+ <span class="tab-emoji">${p.emoji}</span>
887
+ <span class="tab-name">${p.name}</span>
888
+ <span class="dot dot-ok"></span>
889
+ </button>`;
890
+ }).join("");
891
+
892
+ // ── Build OAuth panels ───────────────────────────────────────────────────────
893
+ const oauthPanels = platforms.map((p, i) => {
894
  const allSet = p.envVars.every((v) => v.set);
895
  const anySet = p.envVars.some((v) => v.set);
896
 
 
947
  </div>
948
  <div class="copy-block">
949
  <span class="copy-block-text">${p.callbackUrl}</span>
950
+ <button class="btn-copy btn-copy-white" onclick="copy('${p.callbackUrl}',this)">Copy</button>
951
  </div>
952
 
953
  <div class="section-label">Space Secrets to Add
 
955
  </div>
956
  <div class="env-list">${envRows}</div>
957
 
958
+ <a href="${settingsUrl}" target="_blank" rel="noopener" class="cta-btn">
959
  Open Space Settings β†’ Variables &amp; Secrets
960
  </a>
961
  <p class="hint-final">After adding all secrets, click <strong>Restart Space</strong> for them to take effect.</p>
962
  </div>`;
963
  }).join("");
964
 
965
+ // ── Build direct-connect panels ──────────────────────────────────────────────
966
+ const directPanels = directPlatforms.map((p, i) => {
967
+ const idx = platforms.length + i;
968
+
969
+ const stepsList = p.steps.map((s, si) =>
970
+ `<div class="step">
971
+ <div class="step-num">${si + 1}</div>
972
+ <div>
973
+ <div class="step-title">${s.title}</div>
974
+ <div class="step-body">${s.body}</div>
975
+ </div>
976
+ </div>`
977
+ ).join("");
978
+
979
+ const fieldRows = (p.fields || []).map((f) =>
980
+ `<div class="env-row">
981
+ <div class="env-info">
982
+ <code class="env-name">${f.label}</code>
983
+ <span class="env-desc">${f.hint}</span>
984
+ </div>
985
+ <div class="env-actions">
986
+ <span class="badge ok">READY</span>
987
+ </div>
988
+ </div>`
989
+ ).join("");
990
+
991
+ return `<div class="panel" id="panel-${idx}">
992
+ <div class="panel-head">
993
+ <span class="panel-emoji">${p.emoji}</span>
994
+ <div>
995
+ <div class="panel-title">${p.name}</div>
996
+ <div class="panel-links">
997
+ ${p.docsUrl ? `<a href="${p.docsUrl}" target="_blank" rel="noopener">Official Docs β†’</a>` : ""}
998
+ </div>
999
+ </div>
1000
+ </div>
1001
+
1002
+ <div class="banner banner-ok">No developer portal needed β€” connects directly inside Postiz.</div>
1003
+
1004
+ <div class="section-label">Setup Steps</div>
1005
+ <div class="steps-list">${stepsList}</div>
1006
+
1007
+ ${fieldRows ? `<div class="section-label">Fields Required in Postiz</div>
1008
+ <div class="env-list">${fieldRows}</div>` : ""}
1009
+
1010
+ <a href="${p.postizUrl}" target="_blank" rel="noopener" class="cta-btn">
1011
+ Open Postiz Integrations β†’
1012
+ </a>
1013
+ <p class="hint-final">Click <strong>Connect</strong> next to ${p.name} in Postiz, fill in the credentials above.</p>
1014
+ </div>`;
1015
+ }).join("");
1016
+
1017
+ const allPlatformIds = [
1018
+ ...platforms.map((p) => p.id),
1019
+ ...directPlatforms.map((p) => p.id),
1020
+ ];
1021
+
1022
  return `<!DOCTYPE html>
1023
  <html lang="en">
1024
  <head>
 
1040
  --warn: #f5c542;
1041
  --bad: #fb7185;
1042
  --accent: #3b82f6;
 
1043
  }
1044
  body {
1045
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
 
1117
  color: var(--muted);
1118
  padding: 6px 10px 10px;
1119
  }
1120
+ .sidebar-divider {
1121
+ height: 1px;
1122
+ background: var(--line);
1123
+ margin: 6px 4px 8px;
1124
+ }
1125
  .plat-tab {
1126
  width: 100%;
1127
  background: none;
1128
+ border: 1px solid transparent;
1129
  color: var(--soft);
1130
  font: inherit;
1131
  font-size: .82rem;
 
1138
  cursor: pointer;
1139
  text-align: left;
1140
  transition: background .12s, color .12s;
 
1141
  }
1142
  .plat-tab:hover { background: var(--panel2); color: var(--text); }
1143
  .plat-tab.active {
 
1168
  .panel-title { font-size: 1.4rem; font-weight: 850; margin-bottom: 6px; }
1169
  .panel-links { display: flex; gap: 14px; flex-wrap: wrap; }
1170
  .panel-links a {
1171
+ color: var(--accent);
1172
  font-size: .76rem;
1173
  font-weight: 700;
1174
  text-decoration: none;
 
1224
  width: 22px;
1225
  height: 22px;
1226
  border-radius: 50%;
1227
+ background: var(--accent);
1228
  color: #fff;
1229
  font-size: .68rem;
1230
  font-weight: 850;
 
1237
  .step-title { font-size: .82rem; font-weight: 800; margin-bottom: 3px; color: var(--text); }
1238
  .step-body { font-size: .78rem; color: var(--soft); line-height: 1.6; }
1239
  .step-body strong { color: var(--text); font-weight: 800; }
1240
+ .step-body em { color: var(--soft); font-style: italic; }
1241
  .step-body code {
1242
  background: var(--panel2);
1243
  border: 1px solid var(--line);
 
1246
  font-size: .85em;
1247
  color: var(--text);
1248
  }
1249
+ .step-body a { color: var(--accent); text-decoration: none; }
1250
  .step-body a:hover { text-decoration: underline; }
1251
 
1252
  /* Callback copy block */
 
1263
  flex: 1;
1264
  font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, monospace;
1265
  font-size: .78rem;
1266
+ color: var(--accent);
1267
  word-break: break-all;
1268
  opacity: .9;
1269
  }
1270
 
1271
+ /* Env var / field rows */
1272
  .env-list { display: flex; flex-direction: column; gap: 5px; }
1273
  .env-row {
1274
  display: flex;
 
1283
  .env-name {
1284
  font-family: ui-monospace, "Cascadia Code", monospace;
1285
  font-size: .78rem;
1286
+ color: var(--accent);
1287
+ background: rgba(59,130,246,.1);
1288
+ border: 1px solid rgba(59,130,246,.2);
1289
  padding: 2px 7px;
1290
  border-radius: 5px;
1291
  width: fit-content;
 
1309
  .badge.ok { color: var(--good); border-color: rgba(34,197,94,.3); background: rgba(34,197,94,.1); }
1310
  .badge.off { color: var(--bad); border-color: rgba(251,113,133,.3); background: rgba(251,113,133,.1); }
1311
 
1312
+ /* Buttons β€” secondary (small copy buttons) */
1313
  .btn-copy {
1314
  background: var(--panel2);
1315
  border: 1px solid var(--line);
 
1326
  }
1327
  .btn-copy:hover { background: var(--line); color: var(--text); }
1328
  .btn-copy.copied { background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.3); color: var(--good); }
1329
+
1330
+ /* White copy button (accent copy block) */
1331
+ .btn-copy-white {
1332
+ background: #ffffff;
1333
+ border: 1px solid #ffffff;
1334
+ color: #0d0c1a;
1335
+ font: inherit;
1336
  font-size: .76rem;
1337
+ font-weight: 800;
1338
  padding: 6px 14px;
1339
+ border-radius: 6px;
1340
+ cursor: pointer;
1341
+ transition: background .12s;
1342
+ flex-shrink: 0;
1343
+ white-space: nowrap;
1344
  }
1345
+ .btn-copy-white:hover { background: #e8e8f0; border-color: #e8e8f0; }
1346
+ .btn-copy-white.copied { background: rgba(34,197,94,.15); border-color: rgba(34,197,94,.4); color: var(--good); }
1347
 
1348
+ /* Primary CTA button β€” white with dark text */
1349
+ .cta-btn {
1350
  display: inline-flex;
1351
  align-items: center;
1352
+ margin-top: 18px;
1353
+ background: #ffffff;
1354
+ color: #0d0c1a;
1355
  text-decoration: none;
1356
+ padding: 10px 22px;
1357
  border-radius: 8px;
1358
  font-size: .82rem;
1359
  font-weight: 850;
1360
+ border: none;
1361
+ transition: background .15s;
1362
+ letter-spacing: .01em;
1363
  }
1364
+ .cta-btn:hover { background: #e8e8f0; }
1365
+
1366
  .hint-final {
1367
  margin-top: 10px;
1368
  font-size: .74rem;
 
1387
  gap: 4px;
1388
  }
1389
  .sidebar-label { display: none; }
1390
+ .sidebar-divider { display: none; }
1391
  .plat-tab { width: auto; flex: 0 0 auto; }
1392
  .tab-name { display: none; }
1393
  .main { padding: 16px; }
 
1398
  <div class="topbar">
1399
  <a class="topbar-back" href="/">← Dashboard</a>
1400
  <span class="topbar-title">Platform Setup Guide</span>
1401
+ <span class="topbar-count">${configuredCount} / ${platforms.length} OAuth configured</span>
1402
  </div>
1403
  <div class="layout">
1404
  <nav class="sidebar">
1405
  <div class="sidebar-label">OAuth Platforms</div>
1406
+ ${oauthSidebarItems}
1407
+ <div class="sidebar-divider"></div>
1408
+ <div class="sidebar-label">Direct Connect</div>
1409
+ ${directSidebarItems}
1410
  </nav>
1411
  <main class="main">
1412
+ ${oauthPanels}
1413
+ ${directPanels}
1414
  </main>
1415
  </div>
1416
  <script>
1417
+ const PLATFORM_IDS = ${JSON.stringify(allPlatformIds)};
1418
  function show(i) {
1419
  document.querySelectorAll('.plat-tab').forEach((t,j)=>t.classList.toggle('active',j===i));
1420
  document.querySelectorAll('.panel').forEach((p,j)=>p.classList.toggle('active',j===i));