thumbgate 1.5.4 → 1.6.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/src/api/server.js CHANGED
@@ -152,6 +152,9 @@ const {
152
152
  } = require('../../scripts/decision-journal');
153
153
  const {
154
154
  generateDashboard,
155
+ buildReviewSnapshot,
156
+ readDashboardReviewState,
157
+ writeDashboardReviewState,
155
158
  } = require('../../scripts/dashboard');
156
159
  const {
157
160
  buildDashboardRenderSpec,
@@ -216,6 +219,7 @@ const PRO_PAGE_PATH = path.resolve(__dirname, '../../public/pro.html');
216
219
  const DASHBOARD_PAGE_PATH = path.resolve(__dirname, '../../public/dashboard.html');
217
220
  const LESSONS_PAGE_PATH = path.resolve(__dirname, '../../public/lessons.html');
218
221
  const GUIDE_PAGE_PATH = path.resolve(__dirname, '../../public/guide.html');
222
+ const CODEX_PLUGIN_PAGE_PATH = path.resolve(__dirname, '../../public/codex-plugin.html');
219
223
  const COMPARE_PAGE_PATH = path.resolve(__dirname, '../../public/compare.html');
220
224
  const LEARN_PAGE_PATH = path.resolve(__dirname, '../../public/learn.html');
221
225
  const LEARN_DIR = path.resolve(__dirname, '../../public/learn');
@@ -1703,7 +1707,8 @@ body { font-family: var(--font); background: var(--bg); color: var(--text); line
1703
1707
  .container { max-width: 860px; margin: 0 auto; padding: 0 24px; }
1704
1708
  nav { position: sticky; top: 0; z-index: 50; background: rgba(10,10,11,0.85); backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); padding: 14px 0; }
1705
1709
  nav .container { display: flex; justify-content: space-between; align-items: center; }
1706
- .nav-logo { font-weight: 700; font-size: 15px; color: var(--text); text-decoration: none; }
1710
+ .nav-logo { font-weight: 700; font-size: 15px; color: var(--text); text-decoration: none; display: inline-flex; align-items: center; gap: 8px; }
1711
+ .nav-logo .logo-mark { width: 28px; height: 28px; display: block; }
1707
1712
  .nav-links { display: flex; gap: 16px; align-items: center; }
1708
1713
  .nav-links a { color: var(--text-muted); text-decoration: none; font-size: 13px; }
1709
1714
  .nav-links a:hover { color: var(--text); }
@@ -1748,7 +1753,7 @@ nav .container { display: flex; justify-content: space-between; align-items: cen
1748
1753
  </head>
1749
1754
  <body>
1750
1755
  <nav><div class="container">
1751
- <a href="/dashboard" class="nav-logo">👍👎 ThumbGate</a>
1756
+ <a href="/dashboard" class="nav-logo"><img src="/assets/brand/thumbgate-mark-inline.svg" alt="ThumbGate" class="logo-mark" width="28" height="28"><span class="logo-text">ThumbGate</span></a>
1752
1757
  <div class="nav-links">
1753
1758
  <a href="/dashboard">Dashboard</a>
1754
1759
  <a href="/lessons">Lessons</a>
@@ -1896,8 +1901,11 @@ function renderRobotsTxt(runtimeConfig) {
1896
1901
  function renderSitemapXml(runtimeConfig) {
1897
1902
  const entries = [
1898
1903
  { path: '/', changefreq: 'weekly', priority: '1.0' },
1899
- { path: '/pro', changefreq: 'weekly', priority: '0.9' },
1904
+ // /pro consolidated into /#pro-pitch (2026-04-16) — removed from sitemap
1905
+ // so search engines don't chase the 301 instead of indexing the canonical
1906
+ // homepage directly.
1900
1907
  { path: '/llm-context.md', changefreq: 'weekly', priority: '0.8' },
1908
+ { path: '/codex-plugin', changefreq: 'weekly', priority: '0.75' },
1901
1909
  ...THUMBGATE_SEO_SITEMAP_ENTRIES,
1902
1910
  ];
1903
1911
  return [
@@ -2020,6 +2028,7 @@ function renderCheckoutSuccessPage(runtimeConfig) {
2020
2028
  --accent-dark: #8f451f;
2021
2029
  --card: #fffdf9;
2022
2030
  --success: #2f7d4b;
2031
+ --warning: #8f451f;
2023
2032
  --radius: 14px;
2024
2033
  }
2025
2034
  * { box-sizing: border-box; }
@@ -2071,6 +2080,15 @@ function renderCheckoutSuccessPage(runtimeConfig) {
2071
2080
  font-weight: 700;
2072
2081
  margin-bottom: 8px;
2073
2082
  }
2083
+ .email-status {
2084
+ color: var(--muted);
2085
+ font-size: 14px;
2086
+ margin-top: 10px;
2087
+ }
2088
+ .email-status.warning {
2089
+ color: var(--warning);
2090
+ font-weight: 700;
2091
+ }
2074
2092
  pre {
2075
2093
  white-space: pre-wrap;
2076
2094
  word-break: break-word;
@@ -2106,11 +2124,26 @@ function renderCheckoutSuccessPage(runtimeConfig) {
2106
2124
  color: var(--muted);
2107
2125
  font-size: 14px;
2108
2126
  }
2127
+ .brand-header {
2128
+ display: flex;
2129
+ align-items: center;
2130
+ gap: 10px;
2131
+ margin-bottom: 28px;
2132
+ text-decoration: none;
2133
+ color: var(--ink);
2134
+ font-weight: 700;
2135
+ font-size: 16px;
2136
+ letter-spacing: -0.01em;
2137
+ }
2138
+ .brand-header .logo-mark { width: 32px; height: 32px; display: block; }
2109
2139
  </style>
2140
+ <link rel="icon" type="image/png" href="/thumbgate-icon.png">
2141
+ <link rel="apple-touch-icon" href="/assets/brand/thumbgate-mark.svg">
2110
2142
  <script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
2111
2143
  </head>
2112
2144
  <body>
2113
2145
  <main>
2146
+ <a href="/" class="brand-header"><img src="/assets/brand/thumbgate-mark-inline.svg" alt="ThumbGate" class="logo-mark" width="32" height="32"><span class="logo-text">ThumbGate</span></a>
2114
2147
  <span class="eyebrow">ThumbGate Pro</span>
2115
2148
  <h1>Your local Pro dashboard is ready.</h1>
2116
2149
  <p class="lead">This page verifies your Stripe session, provisions the key if needed, and gives you the exact command to save your license and launch your personal local dashboard.</p>
@@ -2118,6 +2151,7 @@ function renderCheckoutSuccessPage(runtimeConfig) {
2118
2151
  <div class="card">
2119
2152
  <div class="status" id="status">Verifying payment and provisioning your key...</div>
2120
2153
  <p class="muted" id="summary">Do not close this tab until the key appears.</p>
2154
+ <p class="email-status" id="email-status">Activation email pending checkout verification.</p>
2121
2155
  <pre id="key-block">Waiting for checkout session...</pre>
2122
2156
  </div>
2123
2157
 
@@ -2129,11 +2163,26 @@ function renderCheckoutSuccessPage(runtimeConfig) {
2129
2163
  </div>
2130
2164
 
2131
2165
  <div class="card">
2132
- <h2>Hosted API setup (optional)</h2>
2166
+ <h2>Use ThumbGate from CI, teammates, and remote agents (optional)</h2>
2167
+ <p>The Hosted API lets anything that can make an HTTP request &mdash; CI jobs, GitHub Actions, teammates' laptops, scheduled cron, or agents running in Docker or Lambda &mdash; push feedback into the same memory pool your local dashboard already reads from.</p>
2168
+
2169
+ <p><strong>When you need this:</strong></p>
2170
+ <ul>
2171
+ <li>You run agents in CI/CD, GitHub Actions, or Docker containers and want their failures captured automatically.</li>
2172
+ <li>Your team wants shared memory &mdash; every teammate's thumbs-down feeds the same prevention rules.</li>
2173
+ <li>You dispatch agents from servers, Lambdas, or scheduled jobs that never touch your laptop.</li>
2174
+ </ul>
2175
+
2176
+ <p><strong>When you can skip this:</strong></p>
2177
+ <ul>
2178
+ <li>You only use ThumbGate from your own laptop &mdash; the local dashboard already handles everything.</li>
2179
+ </ul>
2180
+
2181
+ <p><strong>How to set it up:</strong></p>
2133
2182
  <ol>
2134
- <li>Copy the environment block below into your workflow runner.</li>
2135
- <li>Use the curl example to confirm the hosted API captures an event.</li>
2136
- <li>Keep your key private and rotate by repurchasing or contacting support if needed.</li>
2183
+ <li>Copy the environment block below into your CI or server environment.</li>
2184
+ <li>Use the curl example to confirm the hosted API captures an event end-to-end.</li>
2185
+ <li>Treat the key like any other API secret &mdash; rotate via your billing portal if it leaks.</li>
2137
2186
  </ol>
2138
2187
  <pre id="env-block">Waiting for provisioning...</pre>
2139
2188
  <pre id="curl-block">Waiting for provisioning...</pre>
@@ -2152,6 +2201,7 @@ function renderCheckoutSuccessPage(runtimeConfig) {
2152
2201
  const telemetryEndpoint = '/v1/telemetry/ping';
2153
2202
  const statusEl = document.getElementById('status');
2154
2203
  const summaryEl = document.getElementById('summary');
2204
+ const emailStatusEl = document.getElementById('email-status');
2155
2205
  const keyBlock = document.getElementById('key-block');
2156
2206
  const envBlock = document.getElementById('env-block');
2157
2207
  const curlBlock = document.getElementById('curl-block');
@@ -2269,9 +2319,23 @@ function renderCheckoutSuccessPage(runtimeConfig) {
2269
2319
  sendTelemetryOnce('checkout_paid_confirmed');
2270
2320
  statusEl.textContent = 'ThumbGate Pro activated.';
2271
2321
  const resolvedTraceId = body.traceId || traceId || '';
2322
+ const emailStatus = body.trialEmail || {};
2323
+ const customerEmail = body.customerEmail || (emailStatus && emailStatus.customerEmail) || '';
2272
2324
  summaryEl.textContent = resolvedTraceId
2273
2325
  ? 'Your Pro key is ready. Save it once, launch your local dashboard, and keep the optional hosted snippet for team workflows. Trace: ' + resolvedTraceId + '.'
2274
2326
  : 'Your Pro key is ready. Save it once, launch your local dashboard, and keep the optional hosted snippet for team workflows.';
2327
+ if (emailStatus.status === 'sent' || emailStatus.status === 'already_sent') {
2328
+ emailStatusEl.className = 'email-status';
2329
+ emailStatusEl.textContent = customerEmail
2330
+ ? 'Activation email sent to ' + customerEmail + '.'
2331
+ : 'Activation email sent.';
2332
+ } else if (emailStatus.status === 'skipped' || emailStatus.status === 'failed') {
2333
+ emailStatusEl.className = 'email-status warning';
2334
+ emailStatusEl.textContent = 'Email delivery is not confirmed. Copy the key below now; this page is your activation source of truth.';
2335
+ } else {
2336
+ emailStatusEl.className = 'email-status warning';
2337
+ emailStatusEl.textContent = 'Email delivery status is unknown. Copy the key below now.';
2338
+ }
2275
2339
  keyBlock.textContent = body.apiKey || 'Provisioned, but no key was returned.';
2276
2340
  activateBlock.textContent = body.apiKey
2277
2341
  ? 'npx thumbgate pro --activate --key=' + body.apiKey
@@ -3453,21 +3517,17 @@ async function addContext(){
3453
3517
  }
3454
3518
 
3455
3519
  if (isGetLikeRequest && pathname === '/pro') {
3456
- try {
3457
- servePublicMarketingPage({
3458
- req,
3459
- res,
3460
- parsed,
3461
- hostedConfig,
3462
- isHeadRequest,
3463
- renderHtml: loadProPageHtml,
3464
- extraTelemetry: {
3465
- pageType: 'pro_landing',
3466
- },
3467
- });
3468
- } catch (err) {
3469
- sendText(res, 500, err.message || 'Pro page unavailable');
3470
- }
3520
+ // Consolidated: /pro content now lives inline on `/` as the #pro-pitch
3521
+ // strip (hero-adjacent pricing card). 301 so external links (README,
3522
+ // plugin manifests, guides, compare pages) pass link equity onto the
3523
+ // single canonical landing page. Query string is preserved so UTM
3524
+ // tracking from inbound campaigns still reaches GA/PostHog on `/`.
3525
+ const redirectTarget = `/#pro-pitch${parsed.search || ''}`;
3526
+ res.writeHead(301, {
3527
+ Location: redirectTarget,
3528
+ 'Cache-Control': 'public, max-age=3600',
3529
+ });
3530
+ res.end();
3471
3531
  return;
3472
3532
  }
3473
3533
 
@@ -3481,6 +3541,16 @@ async function addContext(){
3481
3541
  return;
3482
3542
  }
3483
3543
 
3544
+ if (isGetLikeRequest && pathname === '/codex-plugin') {
3545
+ try {
3546
+ const html = fs.readFileSync(CODEX_PLUGIN_PAGE_PATH, 'utf-8');
3547
+ sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
3548
+ } catch {
3549
+ sendJson(res, 404, { error: 'Codex plugin page not found' });
3550
+ }
3551
+ return;
3552
+ }
3553
+
3484
3554
  if (isGetLikeRequest && pathname === '/compare') {
3485
3555
  try {
3486
3556
  const html = fs.readFileSync(COMPARE_PAGE_PATH, 'utf-8');
@@ -3577,6 +3647,7 @@ async function addContext(){
3577
3647
  if (isGetLikeRequest && (
3578
3648
  pathname === '/favicon.ico'
3579
3649
  || pathname === '/thumbgate-logo.png'
3650
+ || pathname === '/thumbgate-icon.png'
3580
3651
  || pathname === '/og.png'
3581
3652
  || pathname === '/apple-touch-icon.png'
3582
3653
  )) {
@@ -3591,7 +3662,7 @@ async function addContext(){
3591
3662
  version: pkg.version,
3592
3663
  status: 'ok',
3593
3664
  docs: 'https://github.com/IgorGanapolsky/ThumbGate',
3594
- endpoints: ['/health', '/dashboard', '/guide', '/compare', '/learn', '/pro', '/v1/feedback/capture', '/v1/feedback/stats', '/v1/feedback/summary', '/v1/lessons/search', '/v1/search', '/v1/documents', '/v1/documents/import', '/v1/documents/{documentId}', '/v1/dashboard', '/v1/dashboard/render-spec', '/v1/decisions/evaluate', '/v1/decisions/outcome', '/v1/decisions/metrics', '/v1/settings/status', '/v1/dpo/export', '/v1/jobs', '/v1/jobs/harness', '/v1/analytics/databricks/export'],
3665
+ endpoints: ['/health', '/dashboard', '/guide', '/codex-plugin', '/compare', '/learn', '/v1/feedback/capture', '/v1/feedback/stats', '/v1/feedback/summary', '/v1/lessons/search', '/v1/search', '/v1/documents', '/v1/documents/import', '/v1/documents/{documentId}', '/v1/dashboard', '/v1/dashboard/render-spec', '/v1/decisions/evaluate', '/v1/decisions/outcome', '/v1/decisions/metrics', '/v1/settings/status', '/v1/dpo/export', '/v1/jobs', '/v1/jobs/harness', '/v1/analytics/databricks/export'],
3595
3666
  }, {}, {
3596
3667
  headOnly: isHeadRequest,
3597
3668
  });
@@ -3708,6 +3779,7 @@ async function addContext(){
3708
3779
  installId: bootstrapBody.installId,
3709
3780
  traceId,
3710
3781
  metadata: analyticsMetadata,
3782
+ appOrigin: hostedConfig.appOrigin,
3711
3783
  });
3712
3784
 
3713
3785
  if (result.url) {
@@ -4332,6 +4404,7 @@ async function addContext(){
4332
4404
  installId: body.installId,
4333
4405
  traceId,
4334
4406
  metadata: analyticsMetadata,
4407
+ appOrigin: hostedConfig.appOrigin,
4335
4408
  });
4336
4409
  sendJson(res, 200, {
4337
4410
  ...result,
@@ -5520,6 +5593,36 @@ async function addContext(){
5520
5593
  return;
5521
5594
  }
5522
5595
 
5596
+ // GET /v1/dashboard/review-state -- incremental review baseline and deltas
5597
+ if (req.method === 'GET' && pathname === '/v1/dashboard/review-state') {
5598
+ const reviewState = readDashboardReviewState(requestFeedbackDir);
5599
+ const data = generateDashboard(requestFeedbackDir, {
5600
+ reviewBaseline: reviewState,
5601
+ authContext: { tier: 'pro' },
5602
+ });
5603
+ sendJson(res, 200, {
5604
+ reviewState,
5605
+ reviewDelta: data.reviewDelta,
5606
+ });
5607
+ return;
5608
+ }
5609
+
5610
+ // POST /v1/dashboard/review-state -- mark current dashboard state as reviewed
5611
+ if (req.method === 'POST' && pathname === '/v1/dashboard/review-state') {
5612
+ const snapshot = buildReviewSnapshot(requestFeedbackDir);
5613
+ writeDashboardReviewState(requestFeedbackDir, snapshot);
5614
+ const data = generateDashboard(requestFeedbackDir, {
5615
+ reviewBaseline: snapshot,
5616
+ authContext: { tier: 'pro' },
5617
+ });
5618
+ sendJson(res, 200, {
5619
+ ok: true,
5620
+ reviewState: snapshot,
5621
+ reviewDelta: data.reviewDelta,
5622
+ });
5623
+ return;
5624
+ }
5625
+
5523
5626
  // GET /v1/dashboard/render-spec -- Constrained hosted dashboard JSON spec
5524
5627
  if (req.method === 'GET' && pathname === '/v1/dashboard/render-spec') {
5525
5628
  let summaryOptions;