thumbgate 1.27.16 โ†’ 1.27.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/.claude-plugin/marketplace.json +85 -0
  2. package/.claude-plugin/plugin.json +3 -4
  3. package/.well-known/llms.txt +12 -33
  4. package/.well-known/mcp/server-card.json +8 -8
  5. package/README.md +30 -215
  6. package/adapters/chatgpt/openapi.yaml +1695 -0
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/gemini/function-declarations.json +0 -1
  9. package/adapters/mcp/server-stdio.js +11 -263
  10. package/adapters/opencode/opencode.json +1 -1
  11. package/bench/thumbgate-bench.json +2 -2
  12. package/bin/cli.js +122 -1421
  13. package/bin/postinstall.js +27 -11
  14. package/config/gate-templates.json +0 -216
  15. package/config/gates/claim-verification.json +0 -12
  16. package/config/gates/default.json +2 -31
  17. package/config/github-about.json +2 -2
  18. package/config/mcp-allowlists.json +13 -23
  19. package/config/merge-quality-checks.json +1 -0
  20. package/config/model-candidates.json +6 -121
  21. package/openapi/openapi.yaml +0 -12
  22. package/package.json +98 -228
  23. package/public/blog.html +4 -4
  24. package/public/codex-plugin.html +20 -72
  25. package/public/compare.html +8 -31
  26. package/public/dashboard.html +166 -930
  27. package/public/federal.html +2 -2
  28. package/public/guide.html +9 -33
  29. package/public/index.html +116 -495
  30. package/public/learn.html +18 -191
  31. package/public/lessons.html +10 -168
  32. package/public/numbers.html +7 -7
  33. package/public/pro.html +11 -34
  34. package/scripts/agent-memory-lifecycle.js +0 -211
  35. package/scripts/agent-readiness.js +3 -167
  36. package/scripts/agent-reward-model.js +1 -53
  37. package/scripts/auto-promote-gates.js +10 -82
  38. package/scripts/auto-wire-hooks.js +0 -14
  39. package/scripts/billing.js +1 -93
  40. package/scripts/build-metadata.js +10 -50
  41. package/scripts/cli-feedback.js +6 -18
  42. package/scripts/cli-schema.js +0 -90
  43. package/scripts/cli-telemetry.js +1 -6
  44. package/scripts/commercial-offer.js +2 -82
  45. package/scripts/context-manager.js +6 -74
  46. package/scripts/dashboard.js +2 -68
  47. package/scripts/durability/step.js +3 -3
  48. package/scripts/export-databricks-bundle.js +1 -5
  49. package/scripts/export-dpo-pairs.js +2 -7
  50. package/scripts/feedback-loop.js +1 -123
  51. package/scripts/feedback-quality.js +0 -87
  52. package/scripts/filesystem-search.js +10 -35
  53. package/scripts/gate-stats.js +0 -89
  54. package/scripts/gates-engine.js +85 -1105
  55. package/scripts/gemini-embedding-policy.js +1 -2
  56. package/scripts/hook-runtime.js +14 -20
  57. package/scripts/hook-thumbgate-cache-updater.js +2 -18
  58. package/scripts/hybrid-feedback-context.js +7 -142
  59. package/scripts/lesson-inference.js +3 -8
  60. package/scripts/lesson-search.js +1 -17
  61. package/scripts/license.js +1 -0
  62. package/scripts/llm-client.js +9 -390
  63. package/scripts/local-model-profile.js +8 -15
  64. package/scripts/memory-scope-readiness.js +0 -159
  65. package/scripts/meta-agent-loop.js +0 -36
  66. package/scripts/operational-integrity.js +5 -39
  67. package/scripts/oss-pr-opportunity-scout.js +5 -35
  68. package/scripts/plausible-server-events.js +6 -9
  69. package/scripts/pro-local-dashboard.js +4 -4
  70. package/scripts/proxy-pointer-rag-guardrails.js +1 -42
  71. package/scripts/rate-limiter.js +13 -64
  72. package/scripts/secret-scanner.js +5 -44
  73. package/scripts/security-scanner.js +10 -260
  74. package/scripts/self-distill-agent.js +1 -3
  75. package/scripts/seo-gsd.js +2 -916
  76. package/scripts/statusline-cache-path.js +2 -17
  77. package/scripts/statusline-local-stats.js +1 -9
  78. package/scripts/statusline-meta.js +2 -28
  79. package/scripts/statusline.sh +4 -20
  80. package/scripts/telemetry-analytics.js +0 -357
  81. package/scripts/thompson-sampling.js +10 -31
  82. package/scripts/thumbgate-bench.js +1 -16
  83. package/scripts/thumbgate-search.js +19 -85
  84. package/scripts/tool-registry.js +1 -169
  85. package/scripts/vector-store.js +0 -45
  86. package/scripts/workflow-sentinel.js +53 -286
  87. package/scripts/workspace-evolver.js +2 -62
  88. package/src/api/server.js +290 -2578
  89. package/.claude/commands/dashboard.md +0 -15
  90. package/.claude/commands/thumbgate-blocked.md +0 -27
  91. package/.claude/commands/thumbgate-dashboard.md +0 -15
  92. package/.claude/commands/thumbgate-doctor.md +0 -30
  93. package/.claude/commands/thumbgate-guard.md +0 -36
  94. package/.claude/commands/thumbgate-protect.md +0 -30
  95. package/.claude/commands/thumbgate-rules.md +0 -30
  96. package/.well-known/agentic-verify.txt +0 -1
  97. package/adapters/gcp/dfcx-webhook-gate.js +0 -295
  98. package/adapters/letta/README.md +0 -41
  99. package/adapters/letta/thumbgate-letta-adapter.js +0 -133
  100. package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
  101. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
  102. package/bench/observability-eval-suite.json +0 -26
  103. package/bin/dashboard-cli.js +0 -7
  104. package/commands/dashboard.md +0 -15
  105. package/commands/thumbgate-dashboard.md +0 -15
  106. package/config/builtin-lessons.json +0 -23
  107. package/config/gate-classifier-routing.json +0 -98
  108. package/config/post-deploy-marketing-pages.json +0 -61
  109. package/public/about.html +0 -162
  110. package/public/agent-manager.html +0 -179
  111. package/public/agents-cost-savings.html +0 -153
  112. package/public/ai-malpractice-prevention.html +0 -818
  113. package/public/assets/brand/github-social-preview.png +0 -0
  114. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  115. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  116. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  117. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  118. package/public/assets/brand/thumbgate-logo-transparent.svg +0 -22
  119. package/public/assets/brand/thumbgate-mark-inline-v3.svg +0 -19
  120. package/public/assets/brand/thumbgate-mark-pro.svg +0 -23
  121. package/public/assets/brand/thumbgate-mark-team.svg +0 -26
  122. package/public/assets/brand/thumbgate-mark.svg +0 -21
  123. package/public/assets/brand/thumbgate-wordmark.svg +0 -20
  124. package/public/assets/claude-thumbgate-statusbar.svg +0 -8
  125. package/public/assets/codex-thumbgate-statusbar-test.svg +0 -9
  126. package/public/assets/legal-intake-control-flow.svg +0 -66
  127. package/public/brand/thumbgate-mark.svg +0 -19
  128. package/public/brand/thumbgate-og.svg +0 -16
  129. package/public/chatgpt-app.html +0 -330
  130. package/public/codex-enterprise.html +0 -123
  131. package/public/js/buyer-intent.js +0 -672
  132. package/public/pricing.html +0 -399
  133. package/scripts/action-receipts.js +0 -324
  134. package/scripts/activation-quickstart.js +0 -187
  135. package/scripts/agent-operations-planner.js +0 -621
  136. package/scripts/ai-component-inventory.js +0 -367
  137. package/scripts/async-eval-observability.js +0 -236
  138. package/scripts/audit.js +0 -65
  139. package/scripts/aws-blocks-guardrails.js +0 -228
  140. package/scripts/classifier-routing.js +0 -130
  141. package/scripts/dashboard-chat.js +0 -332
  142. package/scripts/feedback-aggregate.js +0 -281
  143. package/scripts/feedback-sanitizer.js +0 -105
  144. package/scripts/hob-pack.js +0 -591
  145. package/scripts/hook-stop-anti-claim.js +0 -301
  146. package/scripts/install-shim.js +0 -87
  147. package/scripts/mcp-oauth.js +0 -293
  148. package/scripts/noop-detect.js +0 -285
  149. package/scripts/omlx-smoke.js +0 -192
  150. package/scripts/parallel-workflow-orchestrator.js +0 -293
  151. package/scripts/plan-gate.js +0 -243
  152. package/scripts/plausible-domain-config.js +0 -99
  153. package/scripts/qa-scenario-planner.js +0 -136
  154. package/scripts/repeat-metric.js +0 -137
  155. package/scripts/secret-fixture-tokens.js +0 -61
  156. package/scripts/secret-redaction.js +0 -166
  157. package/scripts/self-harness-optimizer.js +0 -141
  158. package/scripts/self-healing-check.js +0 -193
  159. package/scripts/silent-failure-cluster.js +0 -531
  160. package/scripts/statusline-cache-read.js +0 -57
  161. package/scripts/sync-telemetry-from-prod.js +0 -374
  162. package/scripts/tool-contract-validator.js +0 -76
  163. package/scripts/trajectory-scorer.js +0 -63
  164. package/scripts/verify-marketing-pages-deployed.js +0 -195
  165. package/scripts/visitor-journey.js +0 -172
@@ -5,13 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>ThumbGate Dashboard โ€” Gate Stats, Agent Inventory, Remediations</title>
7
7
  <meta name="description" content="Live ThumbGate dashboard showing gate enforcement stats, agent surface inventory, actionable remediations, prevention impact, and system health for pre-action checks.">
8
- <link rel="canonical" href="https://thumbgate.ai/dashboard">
8
+ <link rel="canonical" href="https://thumbgate-production.up.railway.app/dashboard">
9
9
  <link rel="icon" type="image/png" href="/thumbgate-icon.png">
10
10
  <link rel="apple-touch-icon" href="/assets/brand/thumbgate-mark.svg">
11
11
  <meta name="robots" content="noindex">
12
- <meta name="referrer" content="same-origin">
13
12
  <!-- Privacy-friendly analytics by Plausible -->
14
- <script defer data-domain="thumbgate.ai" src="https://plausible.io/js/script.js"></script>
13
+ <script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
15
14
  <script type="application/ld+json">
16
15
  {
17
16
  "@context": "https://schema.org",
@@ -19,7 +18,7 @@
19
18
  "name": "ThumbGate Dashboard",
20
19
  "applicationCategory": "DeveloperApplication",
21
20
  "operatingSystem": "Cross-platform",
22
- "url": "https://thumbgate.ai/dashboard",
21
+ "url": "https://thumbgate-production.up.railway.app/dashboard",
23
22
  "dateModified": "2026-04-20",
24
23
  "creator": {
25
24
  "@type": "Person",
@@ -103,7 +102,7 @@
103
102
  .result-tags { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
104
103
  .tag { font-size: 11px; background: var(--cyan-dim); color: var(--cyan); padding: 2px 8px; border-radius: 4px; cursor: pointer; transition: background 0.15s, transform 0.1s; border: none; font-family: inherit; }
105
104
  .tag:hover { background: rgba(34,211,238,0.25); transform: translateY(-1px); }
106
- mark { background: #fbbf24; color: #000000; font-weight: bold; border-radius: 2px; padding: 0 3px; }
105
+ mark { background: rgba(34,211,238,0.25); color: var(--cyan); border-radius: 2px; padding: 0 2px; }
107
106
 
108
107
  /* GATES */
109
108
  .gates-section { margin-bottom: 32px; }
@@ -179,74 +178,6 @@
179
178
  .demo-banner a { color: var(--cyan); text-decoration: none; font-weight: 600; }
180
179
  .demo-banner a:hover { text-decoration: underline; }
181
180
 
182
- /* Tooltips styling */
183
- .tooltip-trigger {
184
- display: inline-flex;
185
- align-items: center;
186
- justify-content: center;
187
- width: 14px;
188
- height: 14px;
189
- border-radius: 50%;
190
- background: var(--border);
191
- color: var(--text-muted);
192
- font-size: 9px;
193
- font-weight: bold;
194
- cursor: help;
195
- margin-left: 6px;
196
- position: relative;
197
- vertical-align: middle;
198
- user-select: none;
199
- }
200
- .tooltip-trigger:hover {
201
- background: var(--cyan);
202
- color: var(--bg);
203
- }
204
- .tooltip-trigger .tooltip-content {
205
- visibility: hidden;
206
- opacity: 0;
207
- width: 220px;
208
- background-color: #161618;
209
- color: #e8e8ec;
210
- text-align: left;
211
- border: 1px solid var(--border);
212
- border-radius: 6px;
213
- padding: 8px 12px;
214
- position: absolute;
215
- z-index: 1000;
216
- bottom: 130%;
217
- left: 50%;
218
- transform: translateX(-50%);
219
- transition: opacity 0.15s, visibility 0.15s;
220
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
221
- font-weight: normal;
222
- font-size: 11px;
223
- line-height: 1.4;
224
- white-space: normal;
225
- pointer-events: none;
226
- font-family: var(--font);
227
- text-transform: none;
228
- letter-spacing: normal;
229
- }
230
- .tooltip-trigger .tooltip-content::after {
231
- content: "";
232
- position: absolute;
233
- top: 100%;
234
- left: 50%;
235
- transform: translateX(-50%);
236
- border-width: 5px;
237
- border-style: solid;
238
- border-color: #161618 transparent transparent transparent;
239
- }
240
- .tooltip-trigger:hover .tooltip-content {
241
- visibility: visible;
242
- opacity: 1;
243
- }
244
- #disconnectBtn:hover {
245
- background: rgba(248,113,113,0.1) !important;
246
- border-color: #f87171 !important;
247
- color: #f87171 !important;
248
- }
249
-
250
181
  .empty { text-align: center; padding: 48px; color: var(--text-muted); font-size: 15px; }
251
182
  .loading { text-align: center; padding: 24px; color: var(--text-muted); }
252
183
  h2 { font-size: 20px; font-weight: 700; margin-bottom: 16px; letter-spacing: -0.02em; }
@@ -258,31 +189,11 @@
258
189
  .settings-card .team-value, .origin-value { font-size: 18px; font-weight: 700; color: var(--text); margin-top: 8px; word-break: break-word; }
259
190
  .origin-list, .layer-list, .routing-list { display: flex; flex-direction: column; gap: 12px; }
260
191
  .origin-note, .layer-note, .routing-note { font-size: 13px; color: var(--text-muted); line-height: 1.55; }
261
- .enterprise-chat-layout { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(260px, 0.7fr); gap: 16px; align-items: start; }
262
- .enterprise-chat-box { min-height: 110px; resize: vertical; width: 100%; background: var(--bg-raised); border: 1px solid var(--border); border-radius: 8px; padding: 12px 14px; color: var(--text); font-size: 14px; font-family: var(--font); line-height: 1.5; }
263
- .enterprise-chat-box:focus { outline: none; border-color: var(--cyan); }
264
- .enterprise-answer { margin-top: 14px; background: var(--bg-raised); border: 1px solid var(--border); border-radius: 10px; padding: 16px; min-height: 92px; font-size: 14px; color: var(--text); line-height: 1.65; }
265
- .enterprise-answer.blocked { border-color: rgba(248,113,113,0.45); background: rgba(248,113,113,0.07); }
266
- .enterprise-source-list { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
267
- .enterprise-source { font-size: 11px; border: 1px solid var(--border); border-radius: 999px; padding: 4px 9px; color: var(--text-muted); background: var(--bg-card); }
268
-
269
- .quick-nav-link {
270
- cursor: pointer;
271
- transition: opacity 0.15s ease, color 0.15s ease;
272
- }
273
- .quick-nav-link:hover {
274
- color: var(--cyan);
275
- opacity: 0.95;
276
- }
277
- .quick-nav-link:hover strong {
278
- color: var(--cyan) !important;
279
- text-decoration: underline;
280
- }
281
192
 
282
193
  @media (max-width: 700px) {
283
194
  .stats-grid { grid-template-columns: repeat(2, 1fr); }
284
195
  .search-filters { flex-wrap: wrap; }
285
- .team-grid, .template-grid, .team-columns, .settings-grid, .generated-grid, .inventory-grid, .enterprise-chat-layout { grid-template-columns: 1fr; }
196
+ .team-grid, .template-grid, .team-columns, .settings-grid, .generated-grid, .inventory-grid { grid-template-columns: 1fr; }
286
197
  }
287
198
  </style>
288
199
  </head>
@@ -290,7 +201,7 @@
290
201
 
291
202
  <nav>
292
203
  <div class="container">
293
- <a href="/" class="nav-logo"><img src="/assets/brand/thumbgate-mark-inline-v3.svg" alt="ThumbGate" class="logo-mark" width="28" height="28"><span class="logo-text">ThumbGate Dashboard</span></a>
204
+ <a href="/" 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 Dashboard</span></a>
294
205
  <div class="nav-links">
295
206
  <a href="/dashboard" class="active" style="color:var(--cyan);">Dashboard</a>
296
207
  <a href="/lessons">Lessons</a>
@@ -303,14 +214,13 @@
303
214
  <div class="auth-bar">
304
215
  <div class="container">
305
216
  <div style="margin-bottom:10px;">
306
- <span id="authTitle" style="font-size:14px;font-weight:600;">Connect your API key</span>
217
+ <span style="font-size:14px;font-weight:600;">Connect your API key</span>
307
218
  <span id="authHelp" style="font-size:13px;color:var(--text-muted);margin-left:8px;">Pro auto-connects locally when you launch with <code style="font-family:var(--mono);font-size:12px;background:var(--cyan-dim);color:var(--cyan);padding:2px 6px;border-radius:4px;">npx thumbgate pro</code>. Otherwise paste your <code style="font-family:var(--mono);font-size:12px;background:var(--cyan-dim);color:var(--cyan);padding:2px 6px;border-radius:4px;">THUMBGATE_API_KEY</code> manually.</span>
308
219
  </div>
309
220
  <div style="display:flex;gap:12px;align-items:center;">
310
221
  <input type="password" class="auth-input" id="apiKey" placeholder="Paste your API key here (THUMBGATE_API_KEY or THUMBGATE_PRO_KEY)..." />
311
222
  <button class="btn" id="connectBtn" onclick="connect()">Connect</button>
312
223
  <button class="btn-outline" id="demoBtn" onclick="loadDemo()">Try Demo</button>
313
- <button class="btn-outline" id="disconnectBtn" onclick="disconnectKey()" style="display:none; border-color:#f87171; color:#f87171;">Disconnect Key</button>
314
224
  <span class="auth-status" id="authStatus"></span>
315
225
  </div>
316
226
  </div>
@@ -323,10 +233,10 @@
323
233
  <p style="font-size:12px;color:var(--text-muted);margin-bottom:8px;">Updated: <time datetime="2026-04-20">2026-04-20</time> ยท by <a href="https://github.com/IgorGanapolsky" style="color:inherit;">Igor Ganapolsky</a></p>
324
234
  <p style="font-size:14px;color:var(--text-muted);line-height:1.6;max-width:700px;">What's happening right now? Search memories, inspect active checks, manage your team, and export training data. <span style="color:var(--cyan);font-weight:600;">This is your control plane for AI agent behavior.</span></p>
325
235
  <div style="display:flex;gap:16px;margin-top:12px;font-size:12px;color:var(--text-muted);">
326
- <span class="quick-nav-link" onclick="switchTab('search')">๐Ÿ” <strong style="color:var(--text);">Search</strong> โ€” find any memory</span>
327
- <span class="quick-nav-link" onclick="switchTab('gates')">๐Ÿ›ก๏ธ <strong style="color:var(--text);">Gates</strong> โ€” what's blocking</span>
328
- <span class="quick-nav-link" onclick="switchTab('team')">๐Ÿ‘ฅ <strong style="color:var(--text);">Team</strong> โ€” org metrics</span>
329
- <span class="quick-nav-link" onclick="switchTab('export')">๐Ÿ“ค <strong style="color:var(--text);">Export</strong> โ€” DPO training data</span>
236
+ <span>๐Ÿ” <strong style="color:var(--text);">Search</strong> โ€” find any memory</span>
237
+ <span>๐Ÿ›ก๏ธ <strong style="color:var(--text);">Gates</strong> โ€” what's blocking</span>
238
+ <span>๐Ÿ‘ฅ <strong style="color:var(--text);">Team</strong> โ€” org metrics</span>
239
+ <span>๐Ÿ“ค <strong style="color:var(--text);">Export</strong> โ€” DPO training data</span>
330
240
  </div>
331
241
  </div>
332
242
 
@@ -337,33 +247,16 @@
337
247
 
338
248
  <!-- STATS -->
339
249
  <div class="stats-grid" id="statsGrid">
340
- <a class="stat-card" data-card-action="all" onclick="selectCard(this,'all')" href="/lessons?signal=all" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view all feedback โ†’ Lessons page"><div class="stat-label">Total Feedback <span class="tooltip-trigger" onclick="event.stopPropagation(); event.preventDefault();">?<span class="tooltip-content">Total thumbs-up and thumbs-down signals captured from your AI agent interactions.</span></span></div><div class="stat-value cyan" id="statTotal">โ€”</div></a>
341
- <a class="stat-card" data-card-action="up" onclick="selectCard(this,'up')" href="/lessons?signal=up" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view positive feedback โ†’ Lessons page"><div class="stat-label">๐Ÿ‘ Positive <span class="tooltip-trigger" onclick="event.stopPropagation(); event.preventDefault();">?<span class="tooltip-content">Successful outcomes where the agent's work met expectations (thumbs-up).</span></span></div><div class="stat-value green" id="statPositive">โ€”</div></a>
342
- <a class="stat-card" data-card-action="down" onclick="selectCard(this,'down')" href="/lessons?signal=down" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view negative feedback โ†’ Lessons page"><div class="stat-label">๐Ÿ‘Ž Negative <span class="tooltip-trigger" onclick="event.stopPropagation(); event.preventDefault();">?<span class="tooltip-content">Unsuccessful runs or errors that we need to learn from to prevent repeat failures (thumbs-down).</span></span></div><div class="stat-value red" id="statNegative">โ€”</div></a>
343
- <a class="stat-card" data-card-action="gates" onclick="selectCard(this,'gates');return false;" href="#" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view active checks"><div class="stat-label">Active Gates <span class="tooltip-trigger" onclick="event.stopPropagation(); event.preventDefault();">?<span class="tooltip-content">Pre-action checks actively monitoring and blocking known-bad tool calls before they run.</span></span></div><div class="stat-value cyan" id="statGates">โ€”</div></a>
344
- </div>
345
-
346
- <div class="panel" id="chatPanel" style="margin-bottom:20px;">
347
- <div style="display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin-bottom:10px;">
348
- <h2 style="margin:0;">๐Ÿ’ฌ Chat with your data</h2>
349
- <span style="font-size:13px;color:var(--text-muted);">Ask about your gates, blocks, feedback, and lessons โ€” answered <strong>locally from your own data</strong>, no cloud. (An optional BYO model only kicks in for open-ended questions.)</span>
350
- </div>
351
- <div id="chatMessages" style="max-height:360px;overflow-y:auto;margin-bottom:12px;display:none;padding-right:4px;"></div>
352
- <div style="display:flex;gap:8px;">
353
- <input id="chatInput" class="auth-input" style="flex:1;" placeholder="e.g. What mistakes have we made, and how do we avoid them?" onkeydown="if(event.key==='Enter'){event.preventDefault();sendChat();}" />
354
- <button class="btn" id="chatSend" onclick="sendChat()">Ask</button>
355
- </div>
356
- <div id="chatHint" style="font-size:12px;color:var(--text-muted);margin-top:8px;display:flex;align-items:center;gap:8px;">
357
- <span>Powered by local dashboard data. Optional Gemini/Perplexity keys only expand open-ended analysis.</span>
358
- <input type="password" id="geminiKeyInput" placeholder="Optional GEMINI_API_KEY..." style="background:var(--bg-raised); border:1px solid var(--border); border-radius:4px; padding:4px 8px; color:var(--text); font-family:var(--mono); font-size:11px; flex:1; max-width: 250px;" onkeydown="if(event.key==='Enter'){event.preventDefault();saveGeminiKey();}" />
359
- <button class="btn-outline" style="padding:4px 10px;font-size:11px;border-radius:4px;" onclick="saveGeminiKey()">Save</button>
360
- </div>
250
+ <a class="stat-card" data-card-action="all" onclick="selectCard(this,'all')" href="/lessons" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view all feedback โ†’ Lessons page"><div class="stat-label">Total Feedback</div><div class="stat-value cyan" id="statTotal">โ€”</div></a>
251
+ <a class="stat-card" data-card-action="up" onclick="selectCard(this,'up')" href="/lessons?signal=positive" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view positive feedback โ†’ Lessons page"><div class="stat-label">๐Ÿ‘ Positive</div><div class="stat-value green" id="statPositive">โ€”</div></a>
252
+ <a class="stat-card" data-card-action="down" onclick="selectCard(this,'down')" href="/lessons?signal=negative" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view negative feedback โ†’ Lessons page"><div class="stat-label">๐Ÿ‘Ž Negative</div><div class="stat-value red" id="statNegative">โ€”</div></a>
253
+ <a class="stat-card" data-card-action="gates" onclick="selectCard(this,'gates');return false;" href="#" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view active checks"><div class="stat-label">Active Gates</div><div class="stat-value cyan" id="statGates">โ€”</div></a>
361
254
  </div>
362
255
 
363
256
  <div class="panel" id="reviewDeltaPanel" style="margin-bottom:20px;">
364
257
  <div style="display:flex;justify-content:space-between;gap:16px;align-items:flex-start;flex-wrap:wrap;">
365
258
  <div>
366
- <h3 style="margin:0 0 8px 0;font-size:14px;letter-spacing:0.04em;text-transform:uppercase;color:var(--text-muted);display:inline-flex;align-items:center;">๐Ÿ†• Since Last Review <span class="tooltip-trigger" onclick="event.stopPropagation(); event.preventDefault();">?<span class="tooltip-content">Shows new feedback, negative signals, lessons, and blocks registered since your last baseline checkpoint.</span></span></h3>
259
+ <h3 style="margin:0 0 8px 0;font-size:14px;letter-spacing:0.04em;text-transform:uppercase;color:var(--text-muted);">๐Ÿ†• Since Last Review</h3>
367
260
  <p class="template-summary" id="reviewDeltaHeadline" style="margin-bottom:0;">No review checkpoint yet. Mark the current dashboard as reviewed to start seeing only new changes.</p>
368
261
  </div>
369
262
  <button class="btn-outline" id="reviewCheckpointBtn" onclick="markReviewed()">Mark Current Dashboard Reviewed</button>
@@ -391,8 +284,6 @@
391
284
  <div class="tab active" onclick="switchTab('search')">๐Ÿ” Search Memories</div>
392
285
  <div class="tab" onclick="switchTab('gates')">๐Ÿ›ก๏ธ Active Gates</div>
393
286
  <div class="tab" onclick="switchTab('team')">๐Ÿ‘ฅ Team</div>
394
- <div class="tab" onclick="switchTab('enterprise')">๐Ÿข Governed Data Chat</div>
395
- <div class="tab" onclick="switchTab('ai-inventory')">๐Ÿงฌ AI Inventory</div>
396
287
  <div class="tab" onclick="switchTab('generated')">๐Ÿงฉ Generated Views</div>
397
288
  <div class="tab" onclick="switchTab('settings')">โš™๏ธ Policy Origins</div>
398
289
  <div class="tab" onclick="switchTab('templates')">๐Ÿงฑ Gate Templates</div>
@@ -402,12 +293,6 @@
402
293
 
403
294
  <!-- SEARCH TAB -->
404
295
  <div class="tab-content active" id="tab-search">
405
- <div class="panel" style="margin-bottom:24px;">
406
- <h3 style="margin-top:0;">๐Ÿ“ˆ Feedback Trend (Last 30 Days)</h3>
407
- <div style="position:relative;height:180px;margin-top:12px;">
408
- <canvas id="feedbackTrendChart"></canvas>
409
- </div>
410
- </div>
411
296
  <div class="search-section">
412
297
  <div class="search-bar">
413
298
  <input type="text" class="search-input" id="searchQuery" placeholder="Search memories... (try: git, test, deploy, secrets, database)" onkeydown="if(event.key==='Enter')search()" />
@@ -429,74 +314,6 @@
429
314
  <div class="tab-content" id="tab-gates">
430
315
  <div class="gates-section">
431
316
  <h2>Active Pre-Action Checks</h2>
432
-
433
- <!-- Interactive Gating Workflow Diagram -->
434
- <details class="flow-details" style="margin-bottom: 24px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-card); padding: 16px;" open>
435
- <summary style="font-weight: 600; cursor: pointer; color: var(--cyan); display: flex; align-items: center; gap: 8px; user-select: none;">
436
- <span>๐Ÿ›ก๏ธ How Pre-Action Gating Works (Interactive Flow Diagram)</span>
437
- </summary>
438
- <div style="margin-top:20px; text-align:center;">
439
- <svg viewBox="0 0 800 220" width="100%" height="auto" style="max-width: 800px; font-family: var(--font); font-size: 11px;">
440
- <style>
441
- .flow-node { fill: #1c1c1f; stroke: var(--border); stroke-width: 1.5; rx: 8px; transition: stroke 0.2s, fill 0.2s; }
442
- .flow-node:hover { fill: rgba(34,211,238,0.06); stroke: var(--cyan); cursor: help; }
443
- .flow-text { fill: var(--text); font-weight: 600; pointer-events: none; }
444
- .flow-subtext { fill: var(--text-muted); font-size: 10px; pointer-events: none; }
445
- .flow-arrow { fill: none; stroke: var(--border); stroke-width: 1.5; marker-end: url(#arrow-head); }
446
- </style>
447
-
448
- <defs>
449
- <marker id="arrow-head" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
450
- <path d="M 0 1 L 10 5 L 0 9 z" fill="#8b8b96" />
451
- </marker>
452
- </defs>
453
-
454
- <!-- Step 1: Tool Call -->
455
- <g>
456
- <rect x="10" y="80" width="130" height="60" class="flow-node" />
457
- <text x="75" y="105" text-anchor="middle" class="flow-text">1. Tool Call</text>
458
- <text x="75" y="125" text-anchor="middle" class="flow-subtext">Agent requests action</text>
459
- </g>
460
-
461
- <path d="M 140 110 L 190 110" class="flow-arrow" />
462
-
463
- <!-- Step 2: Hook Intercept -->
464
- <g>
465
- <rect x="200" y="80" width="140" height="60" class="flow-node" />
466
- <text x="270" y="105" text-anchor="middle" class="flow-text">2. PreToolUse Hook</text>
467
- <text x="270" y="125" text-anchor="middle" class="flow-subtext">Intercepts tool execution</text>
468
- </g>
469
-
470
- <path d="M 340 110 L 390 110" class="flow-arrow" />
471
-
472
- <!-- Step 3: Policy / DB Lookup -->
473
- <g>
474
- <rect x="400" y="80" width="150" height="60" class="flow-node" />
475
- <text x="475" y="105" text-anchor="middle" class="flow-text">3. Gate Evaluation</text>
476
- <text x="475" y="125" text-anchor="middle" class="flow-subtext">Validates matching rules</text>
477
- </g>
478
-
479
- <!-- Branch Arrows -->
480
- <path d="M 550 100 L 640 55" class="flow-arrow" />
481
- <path d="M 550 120 L 640 165" class="flow-arrow" />
482
-
483
- <!-- Step 4a: Block -->
484
- <g>
485
- <rect x="650" y="20" width="130" height="60" class="flow-node" style="stroke:#ef4444;" />
486
- <text x="715" y="45" text-anchor="middle" class="flow-text" style="fill:#ef4444;">โŒ BLOCK</text>
487
- <text x="715" y="65" text-anchor="middle" class="flow-subtext">Matches rule -> aborted</text>
488
- </g>
489
-
490
- <!-- Step 4b: Allow -->
491
- <g>
492
- <rect x="650" y="140" width="130" height="60" class="flow-node" style="stroke:#22c55e;" />
493
- <text x="715" y="165" text-anchor="middle" class="flow-text" style="fill:#22c55e;">โœ… ALLOW</text>
494
- <text x="715" y="185" text-anchor="middle" class="flow-subtext">Runs safely & logs</text>
495
- </g>
496
- </svg>
497
- </div>
498
- </details>
499
-
500
317
  <div id="gatesList"><div class="loading">Loading gates...</div></div>
501
318
  </div>
502
319
  </div>
@@ -570,56 +387,6 @@
570
387
  </div>
571
388
  </div>
572
389
 
573
- <!-- ENTERPRISE CHAT TAB -->
574
- <div class="tab-content" id="tab-enterprise">
575
- <div class="templates-section">
576
- <h2>Governed Data Chat</h2>
577
- <p class="template-summary">Ask questions over your local ThumbGate data (lessons, raw feedback via LanceDB vectors, gate stats, receipts). Uses local retrieval plus your configured LLM, including open-source/local endpoints through <code>THUMBGATE_LOCAL_LLM_ENDPOINT</code>. Dialogflow is not the chatbot layer; DFCX/Vertex cards are optional deployment-readiness checks for buyers who run their own Google agent stack.</p>
578
- <div class="enterprise-chat-layout">
579
- <div class="panel">
580
- <h3>Chat With Local ThumbGate Data</h3>
581
- <textarea class="enterprise-chat-box" id="enterpriseChatPrompt" placeholder="Ask: What mistakes are recurring? Which gates blocked the most? Are we running local-only or cloud-integrated?"></textarea>
582
- <div style="display:flex;gap:10px;align-items:center;margin-top:12px;flex-wrap:wrap;">
583
- <button class="btn" id="enterpriseChatBtn" onclick="sendEnterpriseChat()">Ask ThumbGate</button>
584
- <button class="btn-outline" onclick="setEnterprisePrompt('Which gates are blocking risky actions?')">Gates</button>
585
- <button class="btn-outline" onclick="setEnterprisePrompt('What feedback mistakes keep repeating?')">Feedback</button>
586
- <button class="btn-outline" onclick="setEnterprisePrompt('Are we running local-only or cloud-integrated?')">Runtime</button>
587
- </div>
588
- <div class="enterprise-answer" id="enterpriseChatAnswer">Connect your dashboard, then ask about local ThumbGate data.</div>
589
- <div class="enterprise-source-list" id="enterpriseChatSources"></div>
590
- </div>
591
- <div class="panel">
592
- <h3>Runtime Readiness</h3>
593
- <div class="inventory-tools" id="enterpriseStatusCards"><div class="loading">Loading enterprise status...</div></div>
594
- <div class="template-summary" style="margin-top:14px;margin-bottom:0;">The chat itself is local RAG plus your LLM. Open-source/local models are the default enterprise path; DFCX/Vertex readiness is optional evidence for teams that already operate Google agents in their own tenancy.</div>
595
- </div>
596
- </div>
597
- </div>
598
- </div>
599
-
600
- <!-- AI INVENTORY TAB -->
601
- <div class="tab-content" id="tab-ai-inventory">
602
- <div class="templates-section">
603
- <h2>AI Component Inventory</h2>
604
- <p class="template-summary">Enterprise evidence for AI/ML usage: provider SDKs, agent frameworks, vector databases, Vertex/Gemini/Dialogflow CX references, and local model artifacts. Export this as ML-BOM proof before claiming an AI system is production-ready.</p>
605
- <div class="inventory-grid" id="aiInventorySummaryCards"><div class="loading">Scanning AI inventory...</div></div>
606
- <div class="team-columns" style="margin-top:16px;">
607
- <div class="panel">
608
- <h3>Detected Components</h3>
609
- <div class="inventory-tools" id="aiInventoryComponents"><div class="loading">Scanning components...</div></div>
610
- </div>
611
- <div class="panel">
612
- <h3>Export Evidence</h3>
613
- <div class="template-summary" style="margin-bottom:12px;">CLI export:</div>
614
- <pre style="white-space:pre-wrap;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px;font-size:12px;color:var(--text);">npx thumbgate ai-inventory --format=cyclonedx --output=.thumbgate/ai-mlbom.json</pre>
615
- <div class="template-summary" style="margin-top:12px;">API export:</div>
616
- <pre style="white-space:pre-wrap;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px;font-size:12px;color:var(--text);">GET /v1/dashboard/ai-inventory?format=cyclonedx</pre>
617
- <div id="aiInventoryExportNote" class="template-summary" style="margin-top:12px;margin-bottom:0;">Waiting for inventory evidence...</div>
618
- </div>
619
- </div>
620
- </div>
621
- </div>
622
-
623
390
  <!-- GENERATED TAB -->
624
391
  <div class="tab-content" id="tab-generated">
625
392
  <div class="templates-section">
@@ -693,7 +460,13 @@
693
460
  </div>
694
461
 
695
462
  <!-- Charts -->
696
- <div style="margin-bottom:24px;">
463
+ <div class="team-columns" style="margin-bottom:24px;">
464
+ <div class="panel">
465
+ <h3>Feedback Trend (30 days)</h3>
466
+ <div style="position:relative;height:260px;margin-top:12px;">
467
+ <canvas id="feedbackTrendChart"></canvas>
468
+ </div>
469
+ </div>
697
470
  <div class="panel">
698
471
  <h3>Lessons Generated (30 days)</h3>
699
472
  <div style="position:relative;height:260px;margin-top:12px;">
@@ -766,91 +539,6 @@
766
539
  </div>
767
540
 
768
541
  <script>
769
- // TG_TOKEN_SAVINGS โ€” browser shim that mirrors scripts/token-savings.js
770
- // so loadDemo()'s TG_TOKEN_SAVINGS.compute(blockedCalls, deflectedBots)
771
- // call resolves. Pre-2026-05-26 the dashboard referenced this global
772
- // without ever defining it, silently halting loadDemo() at line ~1452.
773
- var TG_TOKEN_SAVINGS = (function () {
774
- var DEFAULT_MODEL_MIX = { 'claude-sonnet-4-5': 0.80, 'claude-opus-4-6': 0.15, 'claude-haiku-4-5': 0.05 };
775
- var DEFAULT_MODEL_PRICES = {
776
- 'claude-sonnet-4-5': { input: 3.0, output: 15.0 },
777
- 'claude-opus-4-6': { input: 5.0, output: 25.0 },
778
- 'claude-haiku-4-5': { input: 1.0, output: 5.0 },
779
- 'gpt-4o': { input: 2.50, output: 10.0 }
780
- };
781
- var DEFAULT_AVG_INPUT_TOKENS = 2000;
782
- var DEFAULT_AVG_OUTPUT_TOKENS = 600;
783
- function clamp(n, fb) { var v = Number(n); return (isFinite(v) && v >= 0) ? v : fb; }
784
- function blended(mix, prices) {
785
- var i = 0, o = 0, w = 0;
786
- for (var m in mix) {
787
- var ww = clamp(mix[m], 0); if (ww <= 0) continue;
788
- var p = prices[m]; if (!p) continue;
789
- i += clamp(p.input, 0) * ww; o += clamp(p.output, 0) * ww; w += ww;
790
- }
791
- return w > 0 ? { input: i / w, output: o / w } : { input: 0, output: 0 };
792
- }
793
- function fmtTokens(n) {
794
- if (!isFinite(n) || n <= 0) return '0';
795
- if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
796
- if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
797
- if (n >= 1e3) return (n / 1e3).toFixed(0) + 'K';
798
- return String(Math.round(n));
799
- }
800
- function fmtDollars(n) {
801
- if (!isFinite(n)) return '$0.00';
802
- if (n >= 1000) return '$' + Math.round(n).toLocaleString('en-US');
803
- if (n >= 100) return '$' + n.toFixed(0);
804
- if (n >= 10) return '$' + n.toFixed(1);
805
- if (n >= 0.01) return '$' + n.toFixed(2);
806
- if (n > 0) return '$' + n.toFixed(4);
807
- return '$0.00';
808
- }
809
- function compute(blockedCalls, deflectedBots, opts) {
810
- opts = opts || {};
811
- var total = clamp(blockedCalls, 0) + clamp(deflectedBots, 0);
812
- var avgIn = clamp(opts.avgInputTokensPerBlock, DEFAULT_AVG_INPUT_TOKENS);
813
- var avgOut = clamp(opts.avgOutputTokensPerBlock, DEFAULT_AVG_OUTPUT_TOKENS);
814
- var mix = (opts.modelMix && Object.keys(opts.modelMix).length) ? opts.modelMix : DEFAULT_MODEL_MIX;
815
- var prices = opts.modelPrices || DEFAULT_MODEL_PRICES;
816
- var b = blended(mix, prices);
817
- var ti = total * avgIn, to = total * avgOut;
818
- var dollars = (ti * b.input + to * b.output) / 1e6;
819
- return {
820
- blockedCalls: clamp(blockedCalls, 0),
821
- deflectedBots: clamp(deflectedBots, 0),
822
- tokensSavedInput: ti,
823
- tokensSavedOutput: to,
824
- tokensSavedTotal: ti + to,
825
- dollarsSaved: dollars,
826
- dollarsSavedDisplay: fmtDollars(dollars),
827
- tokensSavedDisplay: fmtTokens(ti + to),
828
- blendedPricePer1M: b,
829
- modelMix: mix
830
- };
831
- }
832
- return { compute: compute, formatDollars: fmtDollars, formatTokens: fmtTokens };
833
- })();
834
-
835
- // renderTopBlockedGates โ€” defensive stub for the loadDemo() legacy direct
836
- // call. The current dashboard layout surfaces top-blocked gates via the
837
- // team panel (renderTeam โ†’ team.topBlockedGates) and has no dedicated
838
- // #topBlockedGates element. Stub keeps loadDemo() from throwing while
839
- // also rendering into a target element if one is ever added.
840
- function renderTopBlockedGates(gates) {
841
- if (!Array.isArray(gates)) return;
842
- var target = document.getElementById('topBlockedGates')
843
- || document.getElementById('topBlockedList');
844
- if (!target || typeof escHtml !== 'function') return;
845
- target.innerHTML = gates.map(function (g) {
846
- return '<div class="blocked-row"><div><div class="blocked-name">'
847
- + escHtml(g.name || '')
848
- + '</div></div><div class="blocked-meta">'
849
- + (g.blocked || 0)
850
- + '</div></div>';
851
- }).join('');
852
- }
853
-
854
542
  let API_KEY = '';
855
543
  let currentSource = 'all';
856
544
  let isDemo = false;
@@ -860,237 +548,38 @@ let currentGeneratedView = 'team-review';
860
548
  const BOOTSTRAP_API_KEY = __DASHBOARD_BOOTSTRAP_KEY__;
861
549
  const LOCAL_PRO_BOOTSTRAP = __DASHBOARD_BOOTSTRAP_ENABLED__;
862
550
 
863
- let ACTIVE_PROJECT_DIR = '';
864
- try {
865
- var urlParams = new URLSearchParams(window.location.search);
866
- ACTIVE_PROJECT_DIR = urlParams.get('project') || '';
867
- } catch (e) {}
868
-
869
- function preserveProjectQueryParams() {
870
- if (!ACTIVE_PROJECT_DIR) return;
871
- var links = document.querySelectorAll('a[href^="/dashboard"], a[href^="/lessons"], a[href="/"]');
872
- links.forEach(function(link) {
873
- try {
874
- var url = new URL(link.href, window.location.origin);
875
- url.searchParams.set('project', ACTIVE_PROJECT_DIR);
876
- link.href = url.pathname + url.search + url.hash;
877
- } catch (e) {}
878
- });
879
- }
880
-
881
551
  function getHeaders() {
882
- var headers = { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' };
883
- if (ACTIVE_PROJECT_DIR) {
884
- headers['x-thumbgate-project-dir'] = ACTIVE_PROJECT_DIR;
885
- }
886
- return headers;
887
- }
888
-
889
- // --- Chat with your data ---------------------------------------------------
890
- function chatEscape(s) {
891
- return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
892
- return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
893
- });
894
- }
895
- function chatAppend(who, text) {
896
- var messages = document.getElementById('chatMessages');
897
- var row = document.createElement('div');
898
- row.style.cssText = 'margin-bottom:14px;';
899
- row.innerHTML = '<div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:3px;">'
900
- + (who === 'you' ? 'You' : 'ThumbGate') + '</div><div class="chat-body" style="line-height:1.5;">' + chatEscape(text) + '</div>';
901
- messages.appendChild(row);
902
- return row.querySelector('.chat-body');
903
- }
904
- function chatRenderAnswer(a) {
905
- return chatEscape(a)
906
- .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
907
- .replace(/\[(\d+)\]/g, '<sup style="color:var(--cyan);font-weight:600;">[$1]</sup>')
908
- .replace(/\n/g, '<br>');
909
- }
910
- function chatRenderSources(sources, answerText) {
911
- if (!sources || !sources.length) return '';
912
- var citedNums = new Set();
913
- var regex = /\[(\d+)\]/g;
914
- var match;
915
- while ((match = regex.exec(answerText || '')) !== null) {
916
- citedNums.add(parseInt(match[1], 10));
917
- }
918
-
919
- var items = sources.map(function (s, i) {
920
- var num = i + 1;
921
- var isCited = citedNums.has(num);
922
- var label = chatEscape(String(s.title || s.id || '').slice(0, 64));
923
-
924
- var bg = isCited ? 'var(--cyan-dim)' : 'var(--bg-raised)';
925
- var color = isCited ? 'var(--cyan)' : 'var(--text-muted)';
926
- var border = isCited ? '1px solid var(--cyan)' : '1px solid var(--border)';
927
- var weight = isCited ? '600' : 'normal';
928
-
929
- return '<span title="' + label + '" style="display:inline-block;font-size:11px;background:' + bg + ';color:' + color + ';border:' + border + ';font-weight:' + weight + ';padding:2px 7px;border-radius:5px;margin:4px 5px 0 0;">[' + num + '] ' + label + '</span>';
930
- }).join('');
931
-
932
- return '<div style="margin-top:12px;border-top:1px dashed var(--border);padding-top:10px;">' +
933
- '<div style="font-size:11px;color:var(--text-muted);margin-bottom:6px;font-weight:600;">Database logs analyzed to answer your question:</div>' +
934
- items +
935
- '</div>';
936
- }
937
- async function sendChat() {
938
- var input = document.getElementById('chatInput');
939
- var q = (input.value || '').trim();
940
- if (!q) return;
941
- var messages = document.getElementById('chatMessages');
942
- var sendBtn = document.getElementById('chatSend');
943
- messages.style.display = 'block';
944
- chatAppend('you', q);
945
- input.value = '';
946
- sendBtn.disabled = true;
947
- var pending = chatAppend('bot', 'Thinkingโ€ฆ');
948
- try {
949
- var res = await fetch('/v1/chat', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ question: q }) });
950
- var data = await res.json();
951
- if (data && data.ok) {
952
- pending.innerHTML = chatRenderAnswer(data.answer) + chatRenderSources(data.sources, data.answer);
953
- } else if (data && data.error === 'gemini_error') {
954
- pending.innerHTML = '<em style="color:#f87171;">Gemini rejected the key: ' + chatEscape(data.message || 'invalid') +
955
- '<br>Fix: paste a valid key in the "Set GEMINI_API_KEY..." box below (get one at <a href="https://aistudio.google.com/app/apikey" target="_blank" style="color:#67e8f9;">AI Studio</a>) and click Save.</em>';
956
- } else {
957
- pending.innerHTML = '<em style="color:var(--text-muted);">' + chatEscape((data && data.message) || 'Chat is unavailable.') + '</em>';
958
- }
959
- } catch (e) {
960
- pending.innerHTML = '<em style="color:var(--text-muted);">Chat request failed: ' + chatEscape(String((e && e.message) || e)) + '</em>';
961
- } finally {
962
- sendBtn.disabled = false;
963
- messages.scrollTop = messages.scrollHeight;
964
- }
965
- }
966
-
967
- async function saveGeminiKey() {
968
- var input = document.getElementById('geminiKeyInput');
969
- var val = (input.value || '').trim();
970
- if (!val) return;
971
- var oldPlaceholder = input.placeholder;
972
- input.disabled = true;
973
- input.placeholder = 'Saving...';
974
- try {
975
- var res = await fetch('/v1/settings/gemini-key', {
976
- method: 'POST',
977
- headers: getHeaders(),
978
- body: JSON.stringify({ key: val })
979
- });
980
- var data = await res.json();
981
- if (data && data.ok) {
982
- input.value = '';
983
- input.placeholder = 'โœ“ Key saved to .env';
984
- setTimeout(function() {
985
- document.getElementById('chatHint').innerHTML = '<span style="color:var(--green)">โœ“ Key validated. Hybrid (Perplexity/Gemini) supported for chat with your data.</span>';
986
- }, 2000);
987
- } else {
988
- input.placeholder = 'โœ— Failed: ' + (data.message || data.error || 'Unknown error');
989
- }
990
- } catch (e) {
991
- input.placeholder = 'โœ— Network error';
992
- } finally {
993
- input.disabled = false;
994
- setTimeout(function() { if (input.placeholder.startsWith('โœ—')) input.placeholder = oldPlaceholder; }, 3000);
995
- }
552
+ return { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' };
996
553
  }
997
554
 
998
555
  function hasBootstrapKey() {
999
556
  return LOCAL_PRO_BOOTSTRAP && Boolean(BOOTSTRAP_API_KEY);
1000
557
  }
1001
558
 
1002
- async function checkLocalLlmReachable(url) {
1003
- try {
1004
- var controller = new AbortController();
1005
- var timeout = setTimeout(function() { controller.abort(); }, 400);
1006
- // Use no-cors mode to bypass CORS block (only care if network request succeeds/fails)
1007
- await fetch(url, { method: 'GET', signal: controller.signal, mode: 'no-cors' });
1008
- clearTimeout(timeout);
1009
- return true;
1010
- } catch (e) {
1011
- return false;
1012
- }
1013
- }
1014
-
1015
- function disconnectKey() {
1016
- API_KEY = '';
1017
- isDemo = false;
1018
- var input = document.getElementById('apiKey');
1019
- input.value = '';
1020
- input.disabled = false;
1021
- input.placeholder = "Paste your API key here (THUMBGATE_API_KEY or THUMBGATE_PRO_KEY)...";
1022
- var btn = document.getElementById('connectBtn');
1023
- btn.disabled = false;
1024
- document.getElementById('demoBtn').style.display = 'inline-block';
1025
- document.getElementById('disconnectBtn').style.display = 'none';
1026
- document.getElementById('authStatus').className = 'auth-status';
1027
- document.getElementById('authStatus').textContent = 'Disconnected';
1028
- document.getElementById('authTitle').textContent = 'Connect your API key';
1029
- document.getElementById('authHelp').innerHTML = 'Pro auto-connects locally when you launch with <code style="font-family:var(--mono);font-size:12px;background:var(--cyan-dim);color:var(--cyan);padding:2px 6px;border-radius:4px;">npx thumbgate pro</code>. Otherwise paste your <code style="font-family:var(--mono);font-size:12px;background:var(--cyan-dim);color:var(--cyan);padding:2px 6px;border-radius:4px;">THUMBGATE_API_KEY</code> manually.';
1030
- document.getElementById('dashboardContent').style.display = 'none';
1031
- }
1032
-
1033
559
  async function connect(options) {
1034
560
  var opts = options || {};
1035
561
  var input = document.getElementById('apiKey');
1036
562
  API_KEY = String(opts.key || input.value || '').trim();
1037
563
  if (!API_KEY) return;
1038
-
1039
- var isEnterprise = API_KEY.startsWith('tg_op_') || API_KEY.startsWith('tg_creator_');
1040
- var tierName = isEnterprise ? 'Enterprise' : 'Pro';
1041
-
1042
564
  const status = document.getElementById('authStatus');
1043
565
  const btn = document.getElementById('connectBtn');
1044
566
  btn.disabled = true;
1045
567
  status.className = 'auth-status';
1046
- status.textContent = opts.localPro ? `Connecting local dashboard...` : 'Connecting...';
568
+ status.textContent = opts.localPro ? 'Connecting local dashboard...' : 'Connecting...';
1047
569
  try {
1048
570
  const res = await fetch('/v1/feedback/stats', { headers: getHeaders() });
1049
571
  if (!res.ok) throw new Error('Invalid API key');
1050
572
  const data = await res.json();
1051
-
1052
- if (data.tier) {
1053
- isEnterprise = (data.tier === 'Enterprise');
1054
- tierName = data.tier;
1055
- }
1056
-
1057
- if (data.localLlmConfigured) {
1058
- var modelLabel = data.localLlmModel ? ' (' + data.localLlmModel + ')' : '';
1059
- var llmUrl = data.localLlmEndpoint || 'http://localhost:11434/v1';
1060
- document.getElementById('chatHint').innerHTML = '<span style="color:var(--text-muted)">Checking local LLM reachability...</span>';
1061
- checkLocalLlmReachable(llmUrl.replace(/\/v1$/, '')).then(function(reachable) {
1062
- if (reachable) {
1063
- document.getElementById('chatHint').innerHTML = '<span style="color:var(--green)">โœ“ Local LLM' + modelLabel + ' ready. Chat answers are generated locally โ€” no cloud calls.</span>';
1064
- } else {
1065
- document.getElementById('chatHint').innerHTML = '<span style="color:#f59e0b">โš  Ollama is offline. Launch it to chat locally: <code style="font-family:var(--mono);font-size:12px;background:rgba(245,158,11,0.1);color:#f59e0b;padding:2px 6px;border-radius:4px;">ollama run qwen2.5-coder:14b</code></span>';
1066
- }
1067
- });
1068
- } else if (data.geminiKeyStatus === 'validated' || data.geminiValidatedAt) {
1069
- var when = data.geminiValidatedAt ? ' (validated ' + new Date(data.geminiValidatedAt).toLocaleTimeString() + ')' : '';
1070
- document.getElementById('chatHint').innerHTML = '<span style="color:var(--green)">โœ“ Gemini API key validated' + when + '. You can now chat with your data.</span>';
1071
- } else if (data.perplexityConfigured) {
1072
- document.getElementById('chatHint').innerHTML = '<span style="color:var(--green)">โœ“ Perplexity hybrid (local-cloud) key present. Supports hybrid inference for chat with your data.</span>';
1073
- } else if (data.geminiConfigured) {
1074
- document.getElementById('chatHint').innerHTML = '<span style="color:#f59e0b">โš  Gemini key present in .env (click Save below to validate it). Perplexity hybrid also supported for cost/privacy.</span>';
1075
- }
1076
-
1077
573
  status.className = 'auth-status ok';
1078
- status.textContent = opts.localPro ? `โœ“ Local ${tierName} connected` : 'โœ“ Connected';
1079
- document.getElementById('disconnectBtn').style.display = 'inline-block';
1080
- const titleEl = document.getElementById('authTitle');
1081
- if (titleEl) {
1082
- titleEl.textContent = opts.localPro ? `${tierName} License Active` : 'API Key Connected';
1083
- }
574
+ status.textContent = opts.localPro ? 'โœ“ Local Pro dashboard connected' : 'โœ“ Connected';
1084
575
  document.getElementById('dashboardContent').style.display = 'block';
1085
576
  if (opts.localPro) {
1086
- const localProActiveMessage = 'Local Pro is active on this machine. Your dashboard is using the saved license key automatically.';
1087
- const localEnterpriseActiveMessage = 'Local Enterprise is active on this machine. Your dashboard is using the saved license key automatically. Click Disconnect to switch keys.';
1088
577
  input.value = 'local-license';
1089
578
  input.disabled = true;
1090
- input.placeholder = `Local ${tierName} auto-connected`;
579
+ input.placeholder = 'Local Pro auto-connected';
1091
580
  btn.disabled = true;
1092
581
  document.getElementById('demoBtn').style.display = 'none';
1093
- document.getElementById('authHelp').textContent = isEnterprise ? localEnterpriseActiveMessage : localProActiveMessage;
582
+ document.getElementById('authHelp').textContent = 'Local Pro is active on this machine. Your personal dashboard is using the saved license key automatically.';
1094
583
  }
1095
584
  renderStats(data);
1096
585
  setSelectedCard('all');
@@ -1102,12 +591,8 @@ async function connect(options) {
1102
591
  } catch (e) {
1103
592
  status.className = 'auth-status err';
1104
593
  status.textContent = 'โœ— ' + e.message;
1105
- const titleEl = document.getElementById('authTitle');
1106
- if (titleEl) {
1107
- titleEl.textContent = 'Connect your API key';
1108
- }
1109
594
  if (opts.localPro) {
1110
- document.getElementById('authHelp').textContent = `Local ${tierName} bootstrap failed. Paste your THUMBGATE_API_KEY manually or retry the local launcher.`;
595
+ document.getElementById('authHelp').textContent = 'Local Pro bootstrap failed. Paste your THUMBGATE_API_KEY manually or retry the local launcher.';
1111
596
  }
1112
597
  }
1113
598
  if (!opts.localPro) {
@@ -1171,88 +656,24 @@ async function search() {
1171
656
  }
1172
657
 
1173
658
  function renderResult(r) {
1174
- let contextText = r.context || r.text || r.content || '';
1175
- let signal = r.signal || r.type || '';
1176
-
1177
- if (contextText && contextText.trim().startsWith('{')) {
1178
- const originalText = contextText;
1179
- try {
1180
- let cleanText = contextText.trim();
1181
- if (cleanText.endsWith('โ€ฆ')) {
1182
- cleanText = cleanText.slice(0, -1);
1183
- }
1184
- if (cleanText.endsWith(',...')) {
1185
- cleanText = cleanText.slice(0, -4);
1186
- }
1187
- let parsed = null;
1188
- try {
1189
- parsed = JSON.parse(cleanText);
1190
- } catch (_) {
1191
- try { parsed = JSON.parse(cleanText + '"}'); } catch (_) {}
1192
- try { parsed = JSON.parse(cleanText + '}'); } catch (_) {}
1193
- try { parsed = JSON.parse(cleanText + '"} }'); } catch (_) {}
1194
- }
1195
- if (parsed && (parsed.context || parsed.text || parsed.content || parsed.message)) {
1196
- contextText = parsed.context || parsed.text || parsed.content || parsed.message;
1197
- } else {
1198
- let contextMatch = originalText.match(/"context"\s*:\s*"((?:[^"\\]|\\.)*?)"/);
1199
- if (!contextMatch) {
1200
- contextMatch = originalText.match(/"context"\s*:\s*"((?:[^"\\]|\\.)*)/);
1201
- }
1202
- if (!contextMatch || !contextMatch[1]) {
1203
- let otherMatch = originalText.match(/"(?:text|content|message)"\s*:\s*"((?:[^"\\]|\\.)*?)"/);
1204
- if (!otherMatch) {
1205
- otherMatch = originalText.match(/"(?:text|content|message)"\s*:\s*"((?:[^"\\]|\\.)*)/);
1206
- }
1207
- if (otherMatch && otherMatch[1]) {
1208
- contextMatch = otherMatch;
1209
- }
1210
- }
1211
- if (contextMatch && contextMatch[1]) {
1212
- let val = contextMatch[1];
1213
- val = val.replace(/(?:"\s*,\s*"[^"]*"\s*:?|"\s*,\s*|"\s*\}?\s*|\s*,\s*|\s*\}?\s*)$/, '');
1214
- val = val.replace(/(?:\.\.\.|โ€ฆ|",\.\.\.|",โ€ฆ|"\s*,\s*\.\.\.|"\s*,\s*โ€ฆ)$/, '');
1215
- contextText = val.replace(/\\"/g, '"').replace(/\\\\/g, '\\');
1216
- }
1217
- }
1218
- if (parsed && parsed.signal) {
1219
- signal = parsed.signal;
1220
- } else {
1221
- const signalMatch = originalText.match(/"signal"\s*:\s*"([^"]+)"/);
1222
- if (signalMatch && !signal) {
1223
- signal = signalMatch[1];
1224
- }
1225
- }
1226
- } catch (_) {}
1227
- }
1228
-
659
+ const signal = r.signal || r.type || '';
1229
660
  const isUp = signal === 'up' || signal === 'positive' || signal === 'thumbs_up';
1230
661
  const isDown = signal === 'down' || signal === 'negative' || signal === 'thumbs_down';
1231
662
  const signalClass = isUp ? 'up' : isDown ? 'down' : '';
1232
663
  const signalLabel = isUp ? '๐Ÿ‘ Positive' : isDown ? '๐Ÿ‘Ž Negative' : signal;
1233
-
1234
- let displayTitle = r.title || contextText || '';
1235
- if (displayTitle && displayTitle.startsWith('feedback_')) {
1236
- const parts = displayTitle.split('_');
1237
- if (parts.length >= 3) {
1238
- const type = parts[1];
1239
- const id = parts.slice(2).join('_');
1240
- displayTitle = (type === 'positive' ? '๐Ÿ‘' : '๐Ÿ‘Ž') + ' Feedback (' + id + ')';
1241
- }
1242
- }
1243
-
664
+ const title = r.title || r.context || r.text || '';
665
+ const context = r.context || r.text || r.content || '';
1244
666
  const tags = r.tags || [];
1245
667
  const date = r.timestamp ? new Date(r.timestamp).toLocaleDateString() : '';
1246
-
1247
668
  return '<div class="result-card">' +
1248
669
  '<div class="result-header">' +
1249
670
  (signalLabel ? '<span class="result-signal ' + signalClass + '">' + signalLabel + '</span>' : '<span></span>') +
1250
671
  '<span class="result-date">' + date + '</span>' +
1251
672
  '</div>' +
1252
- (displayTitle ? '<div class="result-title">' + highlightText(displayTitle) + '</div>' : '') +
1253
- (contextText && contextText !== displayTitle ? '<div class="result-context">' + highlightText(contextText.slice(0, 300)) + '</div>' : '') +
673
+ (title ? '<div class="result-title">' + highlightText(title) + '</div>' : '') +
674
+ (context && context !== title ? '<div class="result-context">' + highlightText(context.slice(0, 300)) + '</div>' : '') +
1254
675
  (tags.length ? '<div class="result-tags">' + tags.map(function(t) {
1255
- return '<button type="button" class="tag" data-tag="' + encodeURIComponent(String(t)) + '" title="' + escAttr('Search for ' + t) + '">' + highlightText(t) + '</button>';
676
+ return '<button type="button" class="tag" data-tag="' + encodeURIComponent(String(t)) + '" title="' + escAttr('Search for ' + t) + '">' + escHtml(t) + '</button>';
1256
677
  }).join('') + '</div>' : '') +
1257
678
  '</div>';
1258
679
  }
@@ -1329,23 +750,10 @@ function setSource(el, source) {
1329
750
  function switchTab(name) {
1330
751
  document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
1331
752
  document.querySelectorAll('.tab-content').forEach(function(c) { c.classList.remove('active'); });
1332
- // Scope the header lookup to .tab โ€” the prior selector
1333
- // [onclick*="<name>"] also matched the stat-cards (which carry onclick
1334
- // attributes like selectCard(this,'gates')), and a stat-card appears
1335
- // before the tab header in DOM order, so for 'gates' the wrong element
1336
- // (the card) got the .active class and the tab header stayed dormant.
1337
- var tabEl = document.querySelector('.tab[onclick*="' + name + '"]');
753
+ var tabEl = document.querySelector('[onclick*="' + name + '"]');
1338
754
  var contentEl = document.getElementById('tab-' + name);
1339
755
  if (tabEl) tabEl.classList.add('active');
1340
- if (contentEl) {
1341
- contentEl.classList.add('active');
1342
- // Stat-card clicks fire switchTab from above the fold; without this scroll
1343
- // the user sees "nothing happen" because the just-activated content sits
1344
- // below the viewport. Same class of bug as the /lessons tile fix in #2268.
1345
- try {
1346
- contentEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
1347
- } catch (_e) { /* older browsers without smooth-scroll: no-op */ }
1348
- }
756
+ if (contentEl) contentEl.classList.add('active');
1349
757
  // Sync URL hash so deep-links stay shareable without scroll jump
1350
758
  try {
1351
759
  if (('#' + name) !== window.location.hash) {
@@ -1357,11 +765,11 @@ function switchTab(name) {
1357
765
  /**
1358
766
  * Resolve deep-link tab target from URL hash or query string.
1359
767
  * Supports: /dashboard#insights, /dashboard?tab=gates, /dashboard#tab-export.
1360
- * Valid targets match tab-content ids (search, gates, team, enterprise,
1361
- * ai-inventory, generated, settings, templates, insights, export).
768
+ * Valid targets match tab-content ids (search, gates, team, generated,
769
+ * settings, templates, insights, export).
1362
770
  */
1363
771
  function getDeepLinkTab() {
1364
- var valid = ['search', 'gates', 'team', 'enterprise', 'ai-inventory', 'generated', 'settings', 'templates', 'insights', 'export'];
772
+ var valid = ['search', 'gates', 'team', 'generated', 'settings', 'templates', 'insights', 'export'];
1365
773
  var raw = (window.location.hash || '').replace(/^#/, '').replace(/^tab-/, '');
1366
774
  if (!raw) {
1367
775
  try {
@@ -1528,14 +936,9 @@ async function markReviewed() {
1528
936
  try {
1529
937
  var res = await fetch('/v1/dashboard/review-state', {
1530
938
  method: 'POST',
1531
- headers: getHeaders(),
1532
- body: JSON.stringify({ reviewedAt: new Date().toISOString() })
939
+ headers: getHeaders()
1533
940
  });
1534
- if (!res.ok) {
1535
- var errText = '';
1536
- try { errText = await res.text(); } catch (_) { errText = ''; }
1537
- throw new Error(errText || 'Failed to save review checkpoint');
1538
- }
941
+ if (!res.ok) throw new Error('Failed to save review checkpoint');
1539
942
  var body = await res.json();
1540
943
  renderReviewDelta(body.reviewDelta || {});
1541
944
  } catch (e) {
@@ -1573,68 +976,6 @@ function renderDashboardData(data) {
1573
976
  renderRegulatedProof(data.regulatedProof || {});
1574
977
  renderTemplates(data.templateLibrary || {});
1575
978
  renderInsights(data);
1576
- loadAiInventory();
1577
- loadEnterpriseDataChatStatus();
1578
- }
1579
-
1580
- async function loadAiInventory() {
1581
- var summaryEl = document.getElementById('aiInventorySummaryCards');
1582
- var componentsEl = document.getElementById('aiInventoryComponents');
1583
- var exportEl = document.getElementById('aiInventoryExportNote');
1584
- if (!summaryEl || !componentsEl) return;
1585
-
1586
- try {
1587
- var res = await fetch('/v1/dashboard/ai-inventory', { headers: getHeaders() });
1588
- if (!res.ok) {
1589
- var text = await res.text();
1590
- throw new Error(text || 'Failed to load AI inventory');
1591
- }
1592
- var inventory = await res.json();
1593
- renderAiInventory(inventory);
1594
- } catch (e) {
1595
- var message = e && e.message ? e.message : 'Failed to load AI inventory';
1596
- setMessageState(summaryEl, 'empty', message);
1597
- setMessageState(componentsEl, 'empty', message);
1598
- if (exportEl) exportEl.textContent = message;
1599
- }
1600
- }
1601
-
1602
- function renderAiInventory(inventory) {
1603
- var summaryEl = document.getElementById('aiInventorySummaryCards');
1604
- var componentsEl = document.getElementById('aiInventoryComponents');
1605
- var exportEl = document.getElementById('aiInventoryExportNote');
1606
- var components = Array.isArray(inventory.components) ? inventory.components : [];
1607
- var byCategory = inventory.summary && inventory.summary.byCategory ? inventory.summary.byCategory : {};
1608
- var modelArtifacts = byCategory.model_artifact || 0;
1609
- var vectorDbs = byCategory.vector_database || 0;
1610
-
1611
- summaryEl.innerHTML = [
1612
- '<div class="team-card"><div class="team-kicker">Files scanned</div><div class="team-value">' + escHtml(String(inventory.filesScanned || 0)) + '</div><div class="team-note">source, manifests, model artifacts</div></div>',
1613
- '<div class="team-card"><div class="team-kicker">AI components</div><div class="team-value">' + escHtml(String(inventory.componentCount || components.length)) + '</div><div class="team-note">inventory evidence items</div></div>',
1614
- '<div class="team-card"><div class="team-kicker">Vector stores</div><div class="team-value">' + escHtml(String(vectorDbs)) + '</div><div class="team-note">retrieval governance surface</div></div>',
1615
- '<div class="team-card"><div class="team-kicker">Model artifacts</div><div class="team-value">' + escHtml(String(modelArtifacts)) + '</div><div class="team-note">local ML-BOM artifacts</div></div>'
1616
- ].join('');
1617
-
1618
- if (!components.length) {
1619
- componentsEl.innerHTML = '<div class="empty">No AI/ML components detected in this project root.</div>';
1620
- } else {
1621
- componentsEl.innerHTML = components.map(function(item) {
1622
- var evidence = Array.isArray(item.evidence) ? item.evidence.slice(0, 4) : [];
1623
- var evidenceHtml = evidence.map(function(e) {
1624
- var loc = e.line ? (e.file + ':' + e.line) : e.file;
1625
- return '<li><code>' + escHtml(loc || 'unknown') + '</code> <span style="color:var(--text-muted);">[' + escHtml(e.kind || 'evidence') + ']</span></li>';
1626
- }).join('');
1627
- return '<div class="tool-card">' +
1628
- '<div class="tool-title">' + escHtml(item.name || item.id || 'AI component') + '</div>' +
1629
- '<div class="tool-meta">' + escHtml(item.category || 'unknown') + ' ยท ' + escHtml(item.ecosystem || 'unknown') + '</div>' +
1630
- '<ul style="margin:8px 0 0 18px;padding:0;">' + evidenceHtml + '</ul>' +
1631
- '</div>';
1632
- }).join('');
1633
- }
1634
-
1635
- if (exportEl) {
1636
- exportEl.textContent = 'Ready: export ' + String(inventory.componentCount || components.length) + ' component(s) as CycloneDX ML-BOM evidence.';
1637
- }
1638
979
  }
1639
980
 
1640
981
  function renderGeneratedViewToolbar(spec) {
@@ -2074,108 +1415,6 @@ function renderTemplates(templateLibrary) {
2074
1415
  : '<div class="empty">No check templates available</div>';
2075
1416
  }
2076
1417
 
2077
- function renderEnterpriseStatus(status) {
2078
- var target = document.getElementById('enterpriseStatusCards');
2079
- if (!target) return;
2080
- var vertex = status && status.vertex ? status.vertex : {};
2081
- var dfcx = status && status.dfcx ? status.dfcx : {};
2082
- var localLlm = status && status.chat ? status.chat.localLlmEndpointConfigured : false;
2083
- var connectionMode = localLlm
2084
- ? 'Local/open-source LLM configured'
2085
- : (dfcx.liveAgentConfigured ? 'Google agent guard configured' : (vertex.configured ? 'Vertex routing configured' : 'Local deterministic fallback'));
2086
- var rows = [
2087
- { name: 'Chat runtime', value: connectionMode, note: localLlm ? 'Using THUMBGATE_LOCAL_LLM_ENDPOINT for local/open-source inference.' : 'Local deterministic summaries work without cloud credentials; configure a local LLM for generated answers.' },
2088
- { name: 'Data boundary', value: 'Local by default', note: 'Dashboard answers are grounded in local ThumbGate data and do not require Google Cloud.' },
2089
- { name: 'Vertex adapter', value: vertex.configured ? 'Configured' : 'Optional', note: vertex.projectId ? ('Project: ' + vertex.projectId + ' ยท ' + (vertex.location || '')) : 'Only needed when the buyer chooses Google-hosted routing.' },
2090
- { name: 'Dialogflow guard adapter', value: dfcx.liveAgentConfigured ? 'Configured' : 'Optional', note: dfcx.verification || 'Only needed for customer-owned Dialogflow CX agent deployments.' },
2091
- { name: 'Legacy gcloud CX command', value: 'Unsupported', note: 'Do not use the old alpha gcloud CX command group; use REST API or console for DFCX proof.' }
2092
- ];
2093
-
2094
- function draw() {
2095
- target.innerHTML = rows.map(function(row) {
2096
- return '<div class="inventory-row"><div><div class="inventory-name">' + escHtml(row.name) + '</div><div class="inventory-subtitle">' + escHtml(row.note) + '</div></div><span class="remediation-action">' + escHtml(row.value) + '</span></div>';
2097
- }).join('');
2098
- }
2099
-
2100
- draw();
2101
-
2102
- if (localLlm) {
2103
- var llmUrl = 'http://localhost:11434/v1';
2104
- checkLocalLlmReachable(llmUrl).then(function(reachable) {
2105
- if (reachable) {
2106
- rows[0].value = 'โœ“ Local LLM Online';
2107
- draw();
2108
- } else {
2109
- rows[0].value = 'โš  Local LLM Offline';
2110
- rows[0].note = 'Ollama is offline. Launch it to chat locally: ollama run qwen2.5-coder:14b';
2111
- draw();
2112
- }
2113
- });
2114
- }
2115
- }
2116
-
2117
- async function loadEnterpriseDataChatStatus() {
2118
- if (!API_KEY || isDemo) {
2119
- renderEnterpriseStatus({ vertex: {}, dfcx: { verification: 'Connect to load local status.' } });
2120
- return;
2121
- }
2122
- try {
2123
- var res = await fetch('/v1/enterprise/data-chat/status', { headers: getHeaders() });
2124
- if (!res.ok) throw new Error('status unavailable');
2125
- renderEnterpriseStatus(await res.json());
2126
- } catch (err) {
2127
- var target = document.getElementById('enterpriseStatusCards');
2128
- if (target) target.innerHTML = '<div class="empty">Enterprise status unavailable.</div>';
2129
- }
2130
- }
2131
-
2132
- function setEnterprisePrompt(prompt) {
2133
- var input = document.getElementById('enterpriseChatPrompt');
2134
- if (input) input.value = prompt;
2135
- }
2136
-
2137
- async function sendEnterpriseChat() {
2138
- var input = document.getElementById('enterpriseChatPrompt');
2139
- var answer = document.getElementById('enterpriseChatAnswer');
2140
- var sources = document.getElementById('enterpriseChatSources');
2141
- var button = document.getElementById('enterpriseChatBtn');
2142
- var prompt = input ? input.value.trim() : '';
2143
- if (!prompt) {
2144
- answer.textContent = 'Ask a question first.';
2145
- return;
2146
- }
2147
- if (!API_KEY) {
2148
- answer.textContent = 'Connect your dashboard before using Enterprise Chat.';
2149
- return;
2150
- }
2151
- button.disabled = true;
2152
- answer.className = 'enterprise-answer';
2153
- answer.textContent = 'Local RAG (LanceDB + lessons) plus your LLM...';
2154
- sources.innerHTML = '';
2155
- try {
2156
- var res = await fetch('/v1/chat', {
2157
- method: 'POST',
2158
- headers: getHeaders(),
2159
- body: JSON.stringify({ question: prompt })
2160
- });
2161
- var data = await res.json();
2162
- if (!res.ok) throw new Error(data.detail || data.error || data.message || 'chat failed');
2163
- answer.className = 'enterprise-answer';
2164
- answer.textContent = data.answer || data.message || 'No answer returned.';
2165
- var list = Array.isArray(data.sources) ? data.sources : [];
2166
- sources.innerHTML = list.map(function(source) {
2167
- var label = typeof source === 'string' ? source : (source && (source.title || source.id || ''));
2168
- return '<span class="enterprise-source">' + escHtml(label) + '</span>';
2169
- }).join('');
2170
- loadEnterpriseDataChatStatus();
2171
- } catch (err) {
2172
- answer.className = 'enterprise-answer blocked';
2173
- answer.textContent = err.message || 'Chat failed. Configure a local LLM endpoint or API key for RAG.';
2174
- } finally {
2175
- button.disabled = false;
2176
- }
2177
- }
2178
-
2179
1418
  document.addEventListener('click', function(event) {
2180
1419
  var tagButton = event.target.closest('.tag[data-tag]');
2181
1420
  if (!tagButton) return;
@@ -2189,65 +1428,71 @@ function loadDemo() {
2189
1428
  // Demo badge in nav
2190
1429
  var logo = document.querySelector('.nav-logo');
2191
1430
  if (!document.getElementById('demoBadge')) {
2192
- logo.insertAdjacentHTML('afterend', '<span class="demo-badge" id="demoBadge">Demo Mode โ€” Jamie M., Partner ยท Litigation Intake</span>');
1431
+ logo.insertAdjacentHTML('afterend', '<span class="demo-badge" id="demoBadge">Demo Mode</span>');
2193
1432
  }
2194
1433
  // Stats
2195
- document.getElementById('statTotal').textContent = '184';
2196
- document.getElementById('statPositive').textContent = '91';
2197
- document.getElementById('statNegative').textContent = '93';
2198
- document.getElementById('statGates').textContent = '14';
1434
+ document.getElementById('statTotal').textContent = '297';
1435
+ document.getElementById('statPositive').textContent = '37';
1436
+ document.getElementById('statNegative').textContent = '260';
1437
+ document.getElementById('statGates').textContent = '21';
2199
1438
  setSelectedCard('all');
2200
- var demoSavings = TG_TOKEN_SAVINGS.compute(93, 42);
1439
+ // Demo: 260 negative-feedback events have produced ~180 actual gate blocks
1440
+ // and ~85 bot deflections on /checkout/pro. Kept as illustrative defaults
1441
+ // so the savings card never reads "0" in demo mode.
1442
+ var demoSavings = TG_TOKEN_SAVINGS.compute(180, 85);
2201
1443
  var tEl = document.getElementById('statTokensSaved');
2202
1444
  var dEl = document.getElementById('statDollarsSaved');
2203
1445
  if (tEl) tEl.textContent = demoSavings.tokensSavedDisplay;
2204
1446
  if (dEl) dEl.textContent = 'โ‰ˆ ' + demoSavings.dollarsSavedDisplay + ' saved';
2205
1447
  renderTopBlockedGates([
2206
- { name: 'gate:block-unauthorized-practice', blocked: 34 },
2207
- { name: 'gate:require-conflict-clearance', blocked: 22 },
2208
- { name: 'gate:block-privileged-egress', blocked: 17 },
2209
- { name: 'gate:require-approved-disclaimer', blocked: 11 },
2210
- { name: 'gate:restrict-model-endpoint', blocked: 6 },
2211
- { name: 'gate:require-attorney-review', blocked: 3 },
1448
+ { name: 'gate:no-force-push', blocked: 47 },
1449
+ { name: 'gate:no-drop-prod', blocked: 31 },
1450
+ { name: 'gate:no-hallucinated-import', blocked: 28 },
1451
+ { name: 'gate:no-secret-commit', blocked: 19 },
1452
+ { name: 'gate:require-tests-before-done', blocked: 14 },
1453
+ { name: 'gate:no-figma-claim-without-check', blocked: 9 },
2212
1454
  ]);
2213
1455
  renderReviewDelta({
2214
1456
  hasBaseline: true,
2215
- reviewedAt: '2026-05-22T09:00:00Z',
2216
- previousHead: 'a4c1e7f2',
2217
- currentHead: 'b8d3f9a1',
2218
- feedbackAdded: 8,
2219
- negativeAdded: 5,
2220
- lessonsAdded: 3,
2221
- blocksAdded: 7,
2222
- warnsAdded: 2,
2223
- headline: 'Since your last review: 8 intake events ยท 5 blocked ยท 3 lessons ยท 7 gate enforcements.',
1457
+ reviewedAt: '2025-06-18T09:00:00Z',
1458
+ previousHead: '9f1a2c3d',
1459
+ currentHead: '5c7e9b1a',
1460
+ feedbackAdded: 6,
1461
+ negativeAdded: 4,
1462
+ lessonsAdded: 2,
1463
+ blocksAdded: 5,
1464
+ warnsAdded: 1,
1465
+ headline: 'Since your last review: 6 feedback events ยท 4 negative ยท 2 lessons ยท 5 gate blocks.',
2224
1466
  latestFeedback: {
2225
- title: 'Intake agent provided jurisdictional recommendation without attorney review',
2226
- timestamp: '2026-05-25T14:20:00Z'
1467
+ title: 'Claimed fix worked without running tests',
1468
+ timestamp: '2025-06-22T10:20:00Z'
2227
1469
  },
2228
1470
  latestLesson: {
2229
- title: 'BLOCKED: Advice-shaped response detected โ€” routed to attorney review queue',
2230
- timestamp: '2026-05-24T11:10:00Z'
1471
+ title: 'MISTAKE: Claimed Figma compliance without visual verification',
1472
+ timestamp: '2025-06-21T08:10:00Z'
2231
1473
  }
2232
1474
  });
2233
1475
  document.getElementById('reviewCheckpointBtn').disabled = true;
2234
1476
  document.getElementById('reviewCheckpointBtn').textContent = 'Connect to save your own review checkpoint';
2235
- // Sample memories โ€” legal AI intake scenarios for law firm pilot demo
1477
+ // Sample memories โ€” realistic scenarios from real agent-driven development
2236
1478
  var demoResults = [
2237
- { signal: 'down', title: 'Intake agent provided outcome prediction to prospect', context: 'Prospect asked about wrongful termination in Florida. Agent said "you likely have a strong case." ThumbGate blocked the advice-shaped response before delivery under ABA Rule 5.5.', tags: ['upl-risk', 'aba-rule-5.5', 'auto-blocked', 'critical'], timestamp: '2026-05-25T14:20:00Z' },
2238
- { signal: 'up', title: 'Conflict pre-check stopped intake before facts collected', context: 'Adverse party matched the synthetic conflict list. ThumbGate halted intake before privileged facts were disclosed, preventing a Rule 1.7/1.9 conflict issue.', tags: ['conflict-check', 'aba-rule-1.7', 'aba-rule-1.9', 'prevention'], timestamp: '2026-05-25T11:45:00Z' },
2239
- { signal: 'down', title: 'Agent attempted to send case summary to external CRM', context: 'Agent tried to push matter IDs and consultation notes to an external CRM. ThumbGate blocked the outbound call for privileged-content markers.', tags: ['privilege-egress', 'aba-rule-1.6', 'auto-blocked', 'critical'], timestamp: '2026-05-24T16:30:00Z' },
2240
- { signal: 'up', title: 'Safe handoff collected neutral facts only', context: 'After a UPL block, the agent pivoted to neutral routing facts: name, contact, practice area, and callback time. No legal conclusion was delivered.', tags: ['safe-handoff', 'compliant-response', 'aba-rule-5.5', 'metric:ROI'], timestamp: '2026-05-24T11:10:00Z' },
2241
- { signal: 'down', title: 'Agent made jurisdictional recommendation without review', context: 'Prospect asked where to file. Agent gave a venue recommendation. ThumbGate blocked it because jurisdiction guidance requires attorney supervision.', tags: ['upl-risk', 'jurisdiction', 'aba-rule-5.5', 'auto-blocked'], timestamp: '2026-05-23T15:20:00Z' },
2242
- { signal: 'up', title: 'Audit export preserved full decision chain', context: 'Export showed matched rule, proposed reply, deny decision, attorney-review route, and UTC timestamp. Compliance can replay the decision.', tags: ['audit-trail', 'compliance', 'aba-opinion-512', 'metric:ROI'], timestamp: '2026-05-23T09:00:00Z' },
2243
- { signal: 'down', title: 'Response missing firm-approved disclaimer', context: 'Agent drafted a prospect reply without the required non-engagement disclaimer. ThumbGate blocked delivery until approved text was added.', tags: ['disclaimer-missing', 'non-engagement', 'aba-rule-5.5', 'auto-blocked'], timestamp: '2026-05-22T14:45:00Z' },
2244
- { signal: 'up', title: 'Model endpoint restricted to approved provider', context: 'Agent tried a consumer model endpoint. ThumbGate enforced the allowlist and rerouted to the firm-approved Azure OpenAI endpoint.', tags: ['model-governance', 'data-handling', 'aba-opinion-512', 'prevention'], timestamp: '2026-05-22T10:30:00Z' },
2245
- { signal: 'down', title: 'Agent disclosed opposing counsel information to prospect', context: 'Agent referenced opposing-counsel information from another matter. ThumbGate blocked the cross-matter leak under Rule 1.6.', tags: ['privilege-egress', 'cross-matter', 'aba-rule-1.6', 'critical'], timestamp: '2026-05-21T16:15:00Z' },
2246
- { signal: 'up', title: 'Conflict check cleared โ€” intake proceeded safely', context: 'Adverse party cleared the synthetic fixture. ThumbGate allowed intake and logged timestamp plus rule version.', tags: ['conflict-check', 'cleared', 'aba-rule-1.7', 'compliant-response'], timestamp: '2026-05-21T11:00:00Z' },
2247
- { signal: 'down', title: 'Intake agent attempted to schedule consultation directly', context: 'Agent tried to book a consultation without attorney review. ThumbGate blocked the routing action pending supervisor approval.', tags: ['routing-governance', 'attorney-review', 'auto-blocked'], timestamp: '2026-05-20T13:30:00Z' },
2248
- { signal: 'up', title: 'Egress rerouted to in-tenant system successfully', context: 'Agent needed to store intake notes. ThumbGate rerouted the write to the firm tenant, keeping privileged content inside the boundary.', tags: ['privilege-egress', 'in-tenant-reroute', 'aba-rule-1.6', 'metric:ROI'], timestamp: '2026-05-20T09:45:00Z' },
2249
- { signal: 'up', title: 'DPO export identified systematic UPL pattern', context: 'Exported 34 blocked intake pairs. 76% were outcome predictions after "do I have a case?" prompts, driving revised intake scripts.', tags: ['dpo-export', 'upl-pattern', 'prevention', 'metric:ROI'], timestamp: '2026-05-19T14:00:00Z' },
2250
- { signal: 'down', title: 'Agent used non-approved model for privilege-sensitive query', context: 'Privileged intake query was routed to an endpoint without enterprise DPA. ThumbGate blocked and logged it for security review.', tags: ['model-governance', 'privilege-risk', 'aba-opinion-512', 'auto-blocked'], timestamp: '2026-05-18T11:20:00Z' }
1479
+ { signal: 'down', title: 'Claimed fix worked without running tests', context: 'Agent announced "fixed and pushed" but never ran the test suite. CI failed on 3 tests. Check now requires test evidence before any completion claim.', tags: ['anti-lying', 'verification-gap', 'ci', 'trust-breach'], timestamp: '2025-06-22T10:20:00Z' },
1480
+ { signal: 'down', title: 'Force-pushed to main and lost teammate commits', context: 'Used git push --force on main to fix a rebase. Overwrote 3 commits from another team member. Had to recover from reflog. Check now blocks all --force pushes to protected branches.', tags: ['git', 'destructive', 'auto-blocked'], timestamp: '2025-06-20T09:15:00Z' },
1481
+ { signal: 'up', title: 'Pre-action check caught .env commit', context: 'Check blocked a git add that included .env with production API keys. Saved from leaking secrets to a public repo.', tags: ['security', 'prevention', 'secrets'], timestamp: '2025-06-19T11:30:00Z' },
1482
+ { signal: 'down', title: 'PR scope creep โ€” 72 changed files for a 2-file fix', context: 'Agent included unrelated formatting changes, config files, and lock file updates in a PR that should have touched 2 files. User had to manually separate the changes.', tags: ['pr-scope', 'scope-creep', 'user-frustration', 'git-workflow'], timestamp: '2025-06-18T14:30:00Z' },
1483
+ { signal: 'up', title: 'Full PR review cycle with on-device verification', context: 'Fixed 3 review findings, verified on physical Android device with fresh debug build, confirmed Delete Account flow end-to-end before marking PR ready.', tags: ['pr-review', 'on-device-verification', 'evidence-based', 'metric:ROI'], timestamp: '2025-06-17T16:45:00Z' },
1484
+ { signal: 'down', title: 'Claimed Figma compliance without visual verification', context: 'Said UI matched Figma designs without actually checking. User pointed out wrong colors, missing spacing, and incorrect font weights. Should have used screenshot comparison.', tags: ['anti-lying', 'figma', 'visual-verification', 'trust-breach'], timestamp: '2025-06-16T08:05:00Z' },
1485
+ { signal: 'down', title: 'Deployed without running tests', context: 'Pushed a refactor to main without running the test suite. Two integration tests failed in CI, blocking the release for 3 hours.', tags: ['ci', 'testing', 'deployment'], timestamp: '2025-06-15T09:15:00Z' },
1486
+ { signal: 'up', title: 'Atomic commits improved bisect speed', context: 'Splitting a large feature into 8 atomic commits made git bisect find the regression in under 2 minutes instead of manual debugging.', tags: ['git', 'debugging', 'evidence-based'], timestamp: '2025-06-14T16:45:00Z' },
1487
+ { signal: 'down', title: 'Deleted production database table', context: 'AI agent ran DROP TABLE on a staging-named table that was actually aliased to production. Added a gate requiring explicit confirmation for any destructive SQL.', tags: ['database', 'destructive', 'critical', 'auto-blocked'], timestamp: '2025-06-13T08:05:00Z' },
1488
+ { signal: 'up', title: 'Read file before editing prevented breaking change', context: 'Always read the target file before making changes. Agent caught that the function signature had changed upstream and adapted the edit accordingly.', tags: ['code-quality', 'prevention', 'evidence-based'], timestamp: '2025-06-12T14:30:00Z' },
1489
+ { signal: 'down', title: 'Pushed to wrong branch โ€” created new PR instead of updating existing', context: 'Agent pushed changes to a new branch instead of the open PR branch. Had to cherry-pick commits and force-close the duplicate PR.', tags: ['git-workflow', 'branch-strategy', 'delegation', 'user-frustration'], timestamp: '2025-06-11T10:20:00Z' },
1490
+ { signal: 'up', title: 'ADO tickets split correctly with parallel execution', context: 'Split frontend/backend tasks, corrected area path and iteration, created investigation subtask, and updated PR description โ€” all done efficiently in parallel.', tags: ['ado-workflow', 'parallel-execution', 'triage', 'metric:ROI'], timestamp: '2025-06-10T13:00:00Z' },
1491
+ { signal: 'down', title: 'Delegated debugging to QA instead of investigating', context: 'Told user to ask QA engineer for error logs instead of investigating the issue directly. Wasted 30 minutes of back-and-forth when the fix was a one-line config change.', tags: ['delegation', 'user-frustration', 'debugging', 'speed'], timestamp: '2025-06-09T15:30:00Z' },
1492
+ { signal: 'down', title: 'iOS build failed โ€” forgot pod install after adding dependency', context: 'Added a new npm package but did not run pod install or rebuild the native project. Build failed on iOS physical device. Metro bundler showed stale cache.', tags: ['ios-build', 'metro', 'speed', 'mobile'], timestamp: '2025-06-08T09:45:00Z' },
1493
+ { signal: 'up', title: 'Prevention rule stopped repeat mistake', context: 'Gate auto-generated from 3 prior "forgot pod install" failures now blocks any npm install that touches native modules without a follow-up pod install step.', tags: ['prevention', 'ios-build', 'auto-generated', 'metric:ROI'], timestamp: '2025-06-07T11:00:00Z' },
1494
+ { signal: 'down', title: 'Claimed performance fix without benchmarking', context: 'Said the optimization reduced render time by 40% but never ran a before/after benchmark. Actual measurement showed no improvement. Gate now requires metrics evidence.', tags: ['anti-lying', 'performance', 'verification-gap', 'measure-first'], timestamp: '2025-06-06T14:20:00Z' },
1495
+ { signal: 'up', title: 'DPO export caught systematic failure pattern', context: 'Exported 47 preference pairs revealing that 80% of negative feedback involved claiming work was done without verification. Led to a new mandatory evidence gate.', tags: ['dpo-export', 'evidence-based', 'prevention', 'metric:ROI'], timestamp: '2025-06-05T10:15:00Z' }
2251
1496
  ];
2252
1497
  isDemo = true;
2253
1498
  demoData = demoResults;
@@ -2261,14 +1506,14 @@ function loadDemo() {
2261
1506
  'style="flex:none;background:#b85c2d;color:#fff;padding:8px 18px;border-radius:8px;text-decoration:none;font-weight:700;font-size:13px;white-space:nowrap;">Go Pro โ€” $19/mo</a>' +
2262
1507
  '</div>';
2263
1508
  document.getElementById('searchResults').innerHTML = upgradeBanner + teaserHtml;
2264
- // Sample gates โ€” legal AI intake enforcement
1509
+ // Sample gates
2265
1510
  var demoGates = [
2266
- { name: 'block-unauthorized-practice', pattern: 'Block advice-shaped intake replies from non-attorney agents (ABA Rule 5.5)', action: 'block' },
2267
- { name: 'require-conflict-clearance', pattern: 'Require adverse-party clearance before facts or scheduling (Rules 1.7/1.9/1.10)', action: 'block' },
2268
- { name: 'block-privileged-egress', pattern: 'Block outbound calls with matter IDs or attorney-client markers (Rule 1.6)', action: 'block' },
2269
- { name: 'require-approved-disclaimer', pattern: 'Require firm-approved non-engagement disclaimer before delivery', action: 'block' },
2270
- { name: 'restrict-model-endpoint', pattern: 'Allow only enterprise-approved model endpoints (ABA Opinion 512)', action: 'block' },
2271
- { name: 'require-attorney-review', pattern: 'Route case assignment and scheduling through attorney review', action: 'warn' }
1511
+ { name: 'no-force-push', pattern: 'Block git push --force to main or master branches', action: 'block' },
1512
+ { name: 'read-before-edit', pattern: 'Must read file contents before any edit_file call', action: 'block' },
1513
+ { name: 'test-before-deploy', pattern: 'Run npm test before any deployment or push to main', action: 'block' },
1514
+ { name: 'no-secrets-in-commits', pattern: 'Block git add on .env, credentials, or API key files', action: 'block' },
1515
+ { name: 'evidence-before-claim', pattern: 'Run verification commands before claiming task completion', action: 'block' },
1516
+ { name: 'atomic-commits', pattern: 'Each commit should contain a single logical change', action: 'warn' }
2272
1517
  ];
2273
1518
  document.getElementById('gatesList').innerHTML = demoGates.map(function(g) {
2274
1519
  return '<div class="gate-card"><div><div class="gate-name">' + escHtml(g.name) + '</div>' +
@@ -2276,144 +1521,141 @@ function loadDemo() {
2276
1521
  '<span class="gate-action ' + g.action + '">' + g.action.toUpperCase() + '</span></div>';
2277
1522
  }).join('');
2278
1523
  renderTeam({
2279
- totalAgents: 4,
2280
- activeAgents: 3,
1524
+ totalAgents: 8,
1525
+ activeAgents: 5,
2281
1526
  windowHours: 24,
2282
- orgAdherenceRate: 96.1,
1527
+ orgAdherenceRate: 92.4,
2283
1528
  riskAgents: [
2284
- { id: 'intake-chatbot-prod', project: 'client-intake', branch: 'litigation-practice', adherenceRate: 88.2 },
2285
- { id: 'intake-chatbot-staging', project: 'client-intake', branch: 'ip-practice-pilot', adherenceRate: 91.7 }
1529
+ { id: 'claude-reviewer', project: 'checkout-flow', branch: 'fix/stripe-timeout', adherenceRate: 67.5 },
1530
+ { id: 'codex-second-pass', project: 'dashboard', branch: 'feat/team-rollout', adherenceRate: 74.2 }
2286
1531
  ],
2287
1532
  topBlockedGates: [
2288
- { gateId: 'block-unauthorized-practice', blocked: 34, warned: 0 },
2289
- { gateId: 'require-conflict-clearance', blocked: 22, warned: 3 }
1533
+ { gateId: 'evidence-before-done', blocked: 12, warned: 1 },
1534
+ { gateId: 'never-force-push-main', blocked: 7, warned: 0 }
2290
1535
  ]
2291
1536
  }, {
2292
1537
  northStar: {
2293
- weeklyTeamsRunningProofBackedWorkflows: 2,
1538
+ weeklyTeamsRunningProofBackedWorkflows: 3,
2294
1539
  paidTeamRuns: 1
2295
1540
  }
2296
1541
  });
2297
1542
  renderPredictive({
2298
1543
  upgradePropensity: {
2299
- pro: { band: 'high', score: 0.84 },
2300
- team: { band: 'high', score: 0.72 }
1544
+ pro: { band: 'high', score: 0.71 },
1545
+ team: { band: 'medium', score: 0.54 }
2301
1546
  },
2302
1547
  revenueForecast: {
2303
- predictedBookedRevenueCents: 8400000,
2304
- incrementalOpportunityCents: 3200000
1548
+ predictedBookedRevenueCents: 12800,
1549
+ incrementalOpportunityCents: 4900
2305
1550
  },
2306
1551
  anomalySummary: {
2307
1552
  count: 2,
2308
1553
  severity: 'warning'
2309
1554
  },
2310
1555
  topCreators: [
2311
- { key: 'innovation_team', opportunityRevenueCents: 4800000 }
1556
+ { key: 'reach_vb', opportunityRevenueCents: 3100 }
2312
1557
  ],
2313
1558
  topSources: [
2314
- { key: 'direct_outreach', opportunityRevenueCents: 2400000 }
1559
+ { key: 'producthunt', opportunityRevenueCents: 1800 }
2315
1560
  ],
2316
1561
  anomalies: [
2317
- { type: 'upl_risk_concentration', message: 'UPL blocks concentrate in litigation intake โ€” add practice-area rules.', severity: 'warning' },
2318
- { type: 'egress_pattern', message: 'Privilege egress attempts cluster after hours โ€” add reviewer coverage.', severity: 'warning' }
1562
+ { type: 'pricing_resistance', message: 'Price sensitivity dominates current loss reasons.', severity: 'warning' },
1563
+ { type: 'creator_underperformance', message: 'Creator reach_vb is generating intent without revenue conversion.', severity: 'warning' }
2319
1564
  ]
2320
1565
  });
2321
1566
  renderAgentInventory({
2322
- profile: 'legal-intake',
2323
- tier: 'enterprise',
2324
- configuredServerCount: 2,
2325
- observedToolCount: 5,
1567
+ profile: 'default',
1568
+ tier: 'builder',
1569
+ configuredServerCount: 3,
1570
+ observedToolCount: 4,
2326
1571
  observedTools: [
2327
- { toolName: 'intake_reply', evaluations: 89, intercepted: 34, allow: 55, deny: 34, warn: 0 },
2328
- { toolName: 'conflict_lookup', evaluations: 67, intercepted: 22, allow: 45, deny: 19, warn: 3 },
2329
- { toolName: 'send_to_crm', evaluations: 41, intercepted: 17, allow: 24, deny: 14, warn: 3 },
2330
- { toolName: 'schedule_consultation', evaluations: 28, intercepted: 3, allow: 25, deny: 0, warn: 3 },
2331
- { toolName: 'export_audit', evaluations: 19, intercepted: 0, allow: 19, deny: 0, warn: 0 }
1572
+ { toolName: 'Bash', evaluations: 120, intercepted: 19, allow: 101, deny: 12, warn: 7 },
1573
+ { toolName: 'Edit', evaluations: 64, intercepted: 6, allow: 58, deny: 2, warn: 4 },
1574
+ { toolName: 'Read', evaluations: 42, intercepted: 0, allow: 42, deny: 0, warn: 0 }
2332
1575
  ],
2333
1576
  policySources: [
2334
- { source: 'legal-intake-rules', count: 14 },
2335
- { source: 'privilege-guard', count: 6 },
2336
- { source: 'conflict-sentinel', count: 4 }
1577
+ { source: 'gates-engine', count: 23 },
1578
+ { source: 'secret-guard', count: 6 },
1579
+ { source: 'workflow-sentinel', count: 4 }
2337
1580
  ]
2338
1581
  });
2339
1582
  renderActionableRemediations([
2340
1583
  {
2341
- title: 'add practice-area-specific UPL rules ยท litigation-intake',
2342
- rationale: 'Litigation intake generates 76% of UPL blocks. Add targeted rules to reduce false positives.',
2343
- action: 'add-practice-area-rules',
1584
+ title: 'tighten verification before response ยท recent-signals',
1585
+ rationale: 'Recent approval rate has dropped below the lifetime baseline, so fast completion claims need tighter proof.',
1586
+ action: 'tighten-verification-before-response',
2344
1587
  },
2345
1588
  {
2346
- title: 'extend conflict check to affiliated entities ยท conflict-detection',
2347
- rationale: 'Conflict fixture covers named parties only. Add affiliates and parents.',
2348
- action: 'extend-conflict-coverage',
1589
+ title: 'audit domain failures ยท git-workflow',
1590
+ rationale: 'Git workflow events are overrepresented in high-risk outcomes and deserve a stronger pre-action gate.',
1591
+ action: 'audit-domain-failures',
2349
1592
  },
2350
1593
  {
2351
- title: 'schedule attorney reviewer for off-hours intake ยท privilege-egress',
2352
- rationale: 'Egress attempts cluster after 6pm. Add a backup reviewer queue.',
2353
- action: 'schedule-off-hours-reviewer',
1594
+ title: 'review and update skill ยท github',
1595
+ rationale: 'GitHub-related failures have crossed the negative-rate threshold, so the skill needs a correction pass.',
1596
+ action: 'review-and-update-skill',
2354
1597
  }
2355
1598
  ]);
2356
1599
  renderSettingsStatus({
2357
1600
  activeLayers: [
2358
1601
  { scope: 'defaults', exists: true, leafCount: 7, sourcePath: null },
2359
- { scope: 'firm', exists: true, leafCount: 6, sourcePath: 'config/firm-intake-policy.json' },
2360
- { scope: 'practice-area', exists: true, leafCount: 4, sourcePath: 'config/litigation-rules.json' },
2361
- { scope: 'pilot', exists: true, leafCount: 3, sourcePath: '.thumbgate/pilot-settings.json' },
2362
- { scope: 'managed', exists: true, leafCount: 5, sourcePath: 'config/thumbgate-settings.managed.json' }
1602
+ { scope: 'user', exists: true, leafCount: 2, sourcePath: '~/.thumbgate/settings.json' },
1603
+ { scope: 'project', exists: true, leafCount: 2, sourcePath: '.thumbgate/settings.json' },
1604
+ { scope: 'local', exists: false, leafCount: 0, sourcePath: '.thumbgate/settings.local.json' },
1605
+ { scope: 'managed', exists: true, leafCount: 3, sourcePath: 'config/thumbgate-settings.managed.json' }
2363
1606
  ],
2364
1607
  resolvedSettings: {
2365
- intake: { defaultProfile: 'legal-intake', enforcementMode: 'strict' },
1608
+ mcp: { defaultProfile: 'essential', readonlySessionProfile: 'readonly' },
2366
1609
  harnesses: { enabled: true, allowRuntimeExecution: true }
2367
1610
  },
2368
1611
  routingPreview: {
2369
- intakeAgent: { profile: 'legal-intake', reason: 'all intake agents use strict legal gates' },
2370
- auditExport: { profile: 'readonly', reason: 'audit export has no firm-system write access' },
2371
- reviewQueue: { profile: 'attorney-review', reason: 'blocked responses route to attorney review' }
1612
+ dashboardTool: { profile: 'readonly', reason: 'dashboard is available in the most restrictive profile that contains it' },
1613
+ defaultSession: { profile: 'essential', reason: 'default auto-routing โ€” essential profile from settings hierarchy' },
1614
+ reviewSession: { profile: 'readonly', reason: 'read-only session detected โ€” routing to readonly profile' }
2372
1615
  },
2373
1616
  origins: [
2374
- { path: 'intake.defaultProfile', value: 'legal-intake', scope: 'firm', sourcePath: 'config/firm-intake-policy.json' },
2375
- { path: 'intake.enforcementMode', value: 'strict', scope: 'firm', sourcePath: 'config/firm-intake-policy.json' },
2376
- { path: 'gates.conflictFixturePath', value: 'fixtures/adverse-parties.json', scope: 'pilot', sourcePath: '.thumbgate/pilot-settings.json' },
2377
- { path: 'gates.privilegeMarkers', value: ['attorney-client', 'work-product', 'matter-id'], scope: 'firm', sourcePath: 'config/firm-intake-policy.json' }
1617
+ { path: 'mcp.defaultProfile', value: 'essential', scope: 'project', sourcePath: '.thumbgate/settings.json' },
1618
+ { path: 'mcp.readonlySessionProfile', value: 'readonly', scope: 'project', sourcePath: '.thumbgate/settings.json' },
1619
+ { path: 'harnesses.allowRuntimeExecution', value: true, scope: 'project', sourcePath: '.thumbgate/settings.json' },
1620
+ { path: 'dashboard.showPolicyOrigins', value: true, scope: 'managed', sourcePath: 'config/thumbgate-settings.managed.json' }
2378
1621
  ]
2379
1622
  });
2380
1623
  renderTemplates({
2381
1624
  total: 6,
2382
1625
  categories: {
2383
- 'UPL Prevention': 2,
2384
- 'Conflict Detection': 1,
2385
- 'Privilege Protection': 1,
2386
- 'Model Governance': 1,
2387
- 'Intake Routing': 1
1626
+ 'Git Safety': 1,
1627
+ 'Verification': 1,
1628
+ 'Agent Honesty': 1,
1629
+ 'Database Safety': 1,
1630
+ 'Secrets Hygiene': 1,
1631
+ 'Positive Reinforcement': 1
2388
1632
  },
2389
1633
  templates: [
2390
- { name: 'Block unauthorized practice of law', category: 'UPL Prevention', signal: '๐Ÿ‘Ž', defaultAction: 'block', severity: 'critical', problem: 'Stops advice-shaped intake replies under ABA Rule 5.5.', roi: 'Prevents malpractice exposure before delivery.', rollout: 'Enable on every AI intake channel.', pattern: '(outcome prediction|jurisdictional recommendation|you (should|could|might) (file|sue|claim))' },
2391
- { name: 'Require approved disclaimer', category: 'UPL Prevention', signal: '๐Ÿ‘Ž', defaultAction: 'block', severity: 'high', problem: 'Requires firm-approved non-engagement language.', roi: 'Reduces inadvertent client-relationship risk.', rollout: 'Load approved disclaimer during pilot.', pattern: '(intake_response|client_reply).*(missing|no).*(disclaimer)' },
2392
- { name: 'Require conflict check before intake', category: 'Conflict Detection', signal: '๐Ÿ‘Ž', defaultAction: 'block', severity: 'critical', problem: 'Requires adverse-party clearance before sensitive facts.', roi: 'Catches conflicts before privileged facts cross walls.', rollout: 'Use conflicts API or synthetic pilot fixture.', pattern: '(collect_case_facts|intake_continue).*(missing|no).*(conflict|clearance)' },
2393
- { name: 'Block privileged content egress', category: 'Privilege Protection', signal: '๐Ÿ‘Ž', defaultAction: 'block', severity: 'critical', problem: 'Blocks outbound actions with privilege markers.', roi: 'Prevents single-action privilege waiver.', rollout: 'Define markers; start with hard block.', pattern: '(send_email|api_call|external_request).*(privileged|attorney[_-]client|matter[_-]id)' },
2394
- { name: 'Restrict model endpoint to approved list', category: 'Model Governance', signal: '๐Ÿ‘Ž', defaultAction: 'block', severity: 'high', problem: 'Blocks unapproved model endpoints.', roi: 'Keeps data inside approved vendor boundaries.', rollout: 'Allowlist endpoints during pilot.', pattern: '(model_call|llm_request).*(unapproved|unknown|consumer)' },
2395
- { name: 'Require attorney review before routing', category: 'Intake Routing', signal: '๐Ÿ‘Ž', defaultAction: 'warn', severity: 'high', problem: 'Requires review before routing or scheduling.', roi: 'Prevents bad prospect routing.', rollout: 'Start strict; relax after pilot evidence.', pattern: '(route_to_attorney|schedule_consultation).*(no|missing).*(review|approval)' }
1634
+ { name: 'Never force-push to main', category: 'Git Safety', signal: '๐Ÿ‘Ž', defaultAction: 'block', severity: 'critical', problem: 'Stops destructive history rewrites on protected branches before they land.', roi: 'Protects every shared repo from the fastest irreversible mistake.', rollout: 'Enable on every team repo on day one.', pattern: 'git\\s+push\\s+(--force|-f)' },
1635
+ { name: 'Never skip tests before commit', category: 'Verification', signal: '๐Ÿ‘Ž', defaultAction: 'block', severity: 'high', problem: 'Requires proof before code leaves the laptop.', roi: 'Cuts review churn and CI rollback time across the team.', rollout: 'Pair with repository-specific test commands.', pattern: 'git\\s+commit' },
1636
+ { name: 'Require evidence before saying done', category: 'Agent Honesty', signal: '๐Ÿ‘Ž', defaultAction: 'block', severity: 'high', problem: 'Prevents unsupported completion claims.', roi: 'Raises trust in autonomous runs.', rollout: 'Use anywhere proof matters more than speed.', pattern: 'completion_claim_without_verification' },
1637
+ { name: 'Protect production SQL', category: 'Database Safety', signal: '๐Ÿ‘Ž', defaultAction: 'block', severity: 'critical', problem: 'Blocks destructive SQL operations on production-like targets.', roi: 'One saved incident pays for the rollout.', rollout: 'Turn on for any team touching live data.', pattern: '(drop|truncate|delete)\\s+.*production' }
2396
1638
  ]
2397
1639
  });
2398
1640
  renderGeneratedView(buildDemoGeneratedViewSpec(currentGeneratedView));
2399
1641
  renderInsights({
2400
- gateStats: { blocked: 93 },
1642
+ gateStats: { blocked: 180 },
2401
1643
  tokenSavings: {
2402
1644
  dollarsSaved: 0,
2403
1645
  dollarsSavedDisplay: '$0.00',
2404
1646
  tokensSavedDisplay: '0',
2405
1647
  blockedCalls: 0,
2406
- modelMix: { 'gpt-4o': 0.5, 'claude-sonnet-4.5': 0.35, 'azure-openai': 0.15 },
2407
- blendedPricePer1M: { input: 5.0, output: 15.0 }
1648
+ modelMix: { 'sonnet-4.5': 0.8, 'opus-4.6': 0.15, 'haiku-4.5': 0.05 },
1649
+ blendedPricePer1M: { input: 6.2, output: 24.1 }
2408
1650
  },
2409
1651
  lessonPipeline: {
2410
1652
  stages: [
2411
- { id: 'feedback', label: 'Intake Events', count: 184, detail: '91 safe / 93 blocked' },
2412
- { id: 'lessons', label: 'Rules Refined', count: 42, detail: '28 UPL patterns / 14 egress patterns' },
2413
- { id: 'gates', label: 'Gates Active', count: 14, detail: '33% of rules become enforcement gates' },
2414
- { id: 'blocked', label: 'Risks Prevented', count: 93, detail: 'UPL, conflict, and privilege violations stopped' }
1653
+ { id: 'feedback', label: 'Feedback Signals', count: 297, detail: '37 up / 260 down' },
1654
+ { id: 'lessons', label: 'Lessons Distilled', count: 94, detail: '72 mistakes / 22 good patterns' },
1655
+ { id: 'gates', label: 'Gates Promoted', count: 21, detail: '22% of lessons become gates' },
1656
+ { id: 'blocked', label: 'Actions Blocked', count: 180, detail: 'Repeat mistakes prevented' }
2415
1657
  ],
2416
- rates: { feedbackToLesson: 23, lessonToGate: 33 }
1658
+ rates: { feedbackToLesson: 32, lessonToGate: 22 }
2417
1659
  },
2418
1660
  feedbackTimeSeries: {
2419
1661
  days: Array.from({ length: 30 }, function(_, index) {
@@ -2437,14 +1679,14 @@ function loadDemo() {
2437
1679
  },
2438
1680
  actionableRemediations: [
2439
1681
  {
2440
- title: 'add litigation-specific UPL rules',
2441
- rationale: '76% of UPL blocks come from litigation intake. Add targeted rules.',
2442
- action: 'add-practice-area-rules'
1682
+ title: 'tighten verification before response ยท recent-signals',
1683
+ rationale: 'Recent approval rate has dropped below the lifetime baseline, so fast completion claims need tighter proof.',
1684
+ action: 'tighten-verification-before-response'
2443
1685
  },
2444
1686
  {
2445
- title: 'extend conflict checks to affiliates',
2446
- rationale: 'Named-party fixture misses affiliates and parents.',
2447
- action: 'extend-conflict-coverage'
1687
+ title: 'audit domain failures ยท git-workflow',
1688
+ rationale: 'Git workflow events are overrepresented in high-risk outcomes and deserve a stronger pre-action gate.',
1689
+ action: 'audit-domain-failures'
2448
1690
  }
2449
1691
  ]
2450
1692
  });
@@ -2452,7 +1694,6 @@ function loadDemo() {
2452
1694
 
2453
1695
  // Auto-load demo on first visit so visitors see the product immediately
2454
1696
  window.addEventListener('DOMContentLoaded', function() {
2455
- preserveProjectQueryParams();
2456
1697
  if (hasBootstrapKey()) {
2457
1698
  connect({ key: BOOTSTRAP_API_KEY, localPro: true });
2458
1699
  return;
@@ -2607,7 +1848,6 @@ function chartDefaults() {
2607
1848
  function renderFeedbackTrendChart(ts) {
2608
1849
  var canvas = document.getElementById('feedbackTrendChart');
2609
1850
  if (!canvas) return;
2610
- if (typeof Chart === 'undefined') return;
2611
1851
  var days = ts.days || [];
2612
1852
  if (feedbackChart) feedbackChart.destroy();
2613
1853
 
@@ -2658,7 +1898,6 @@ function renderFeedbackTrendChart(ts) {
2658
1898
  function renderLessonTrendChart(ts) {
2659
1899
  var canvas = document.getElementById('lessonTrendChart');
2660
1900
  if (!canvas) return;
2661
- if (typeof Chart === 'undefined') return;
2662
1901
  var days = ts.days || [];
2663
1902
  if (lessonChart) lessonChart.destroy();
2664
1903
 
@@ -2717,7 +1956,6 @@ function renderLessonTrendChart(ts) {
2717
1956
  function renderGateAuditChartFromData(gateAudit) {
2718
1957
  var canvas = document.getElementById('gateAuditChart');
2719
1958
  if (!canvas) return;
2720
- if (typeof Chart === 'undefined') return;
2721
1959
  var days = gateAudit.days || [];
2722
1960
  if (gateAuditChart) gateAuditChart.destroy();
2723
1961
 
@@ -2761,8 +1999,6 @@ function renderGateAuditChartFromData(gateAudit) {
2761
1999
  },
2762
2000
  });
2763
2001
  }
2764
-
2765
2002
  </script>
2766
- <script src="/js/buyer-intent.js"></script>
2767
2003
  </body>
2768
2004
  </html>