thumbgate 1.27.18 โ†’ 1.27.19

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 (96) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +4 -3
  3. package/.well-known/agentic-verify.txt +1 -0
  4. package/.well-known/llms.txt +33 -12
  5. package/.well-known/mcp/server-card.json +8 -8
  6. package/README.md +249 -30
  7. package/adapters/chatgpt/openapi.yaml +12 -0
  8. package/adapters/claude/.mcp.json +2 -2
  9. package/adapters/codex/config.toml +2 -2
  10. package/adapters/gemini/function-declarations.json +1 -0
  11. package/adapters/mcp/server-stdio.js +263 -11
  12. package/adapters/opencode/opencode.json +1 -1
  13. package/bench/thumbgate-bench.json +2 -2
  14. package/bin/cli.js +1429 -121
  15. package/bin/postinstall.js +1 -8
  16. package/config/gate-classifier-routing.json +98 -0
  17. package/config/gate-templates.json +216 -0
  18. package/config/gates/claim-verification.json +12 -0
  19. package/config/gates/default.json +31 -2
  20. package/config/github-about.json +2 -2
  21. package/config/mcp-allowlists.json +23 -13
  22. package/config/merge-quality-checks.json +0 -1
  23. package/config/model-candidates.json +121 -6
  24. package/config/post-deploy-marketing-pages.json +80 -0
  25. package/config/tessl-tiles.json +1 -3
  26. package/openapi/openapi.yaml +12 -0
  27. package/package.json +1 -1
  28. package/public/blog.html +4 -4
  29. package/public/codex-plugin.html +72 -20
  30. package/public/compare.html +31 -8
  31. package/public/dashboard.html +930 -166
  32. package/public/federal.html +2 -2
  33. package/public/guide.html +33 -13
  34. package/public/index.html +469 -111
  35. package/public/learn.html +183 -18
  36. package/public/lessons.html +168 -10
  37. package/public/numbers.html +7 -7
  38. package/public/pro.html +34 -11
  39. package/scripts/agent-memory-lifecycle.js +211 -0
  40. package/scripts/agent-readiness.js +20 -3
  41. package/scripts/agent-reward-model.js +53 -1
  42. package/scripts/auto-promote-gates.js +82 -10
  43. package/scripts/auto-wire-hooks.js +14 -0
  44. package/scripts/billing.js +93 -1
  45. package/scripts/bot-detection.js +61 -3
  46. package/scripts/build-metadata.js +50 -10
  47. package/scripts/cli-feedback.js +4 -2
  48. package/scripts/cli-schema.js +97 -0
  49. package/scripts/cli-telemetry.js +6 -1
  50. package/scripts/commercial-offer.js +82 -2
  51. package/scripts/context-manager.js +74 -6
  52. package/scripts/dashboard.js +68 -2
  53. package/scripts/export-databricks-bundle.js +5 -1
  54. package/scripts/export-dpo-pairs.js +7 -2
  55. package/scripts/feedback-loop.js +123 -1
  56. package/scripts/feedback-quality.js +87 -0
  57. package/scripts/filesystem-search.js +35 -10
  58. package/scripts/gate-stats.js +89 -0
  59. package/scripts/gates-engine.js +1176 -85
  60. package/scripts/gemini-embedding-policy.js +2 -1
  61. package/scripts/hook-runtime.js +20 -14
  62. package/scripts/hook-thumbgate-cache-updater.js +18 -2
  63. package/scripts/hybrid-feedback-context.js +142 -7
  64. package/scripts/lesson-inference.js +8 -3
  65. package/scripts/lesson-search.js +17 -1
  66. package/scripts/license.js +10 -10
  67. package/scripts/llm-client.js +169 -4
  68. package/scripts/local-model-profile.js +15 -8
  69. package/scripts/mcp-config.js +7 -1
  70. package/scripts/memory-scope-readiness.js +159 -0
  71. package/scripts/meta-agent-loop.js +36 -0
  72. package/scripts/operational-integrity.js +39 -5
  73. package/scripts/oss-pr-opportunity-scout.js +35 -5
  74. package/scripts/plausible-server-events.js +9 -6
  75. package/scripts/pro-local-dashboard.js +4 -4
  76. package/scripts/proxy-pointer-rag-guardrails.js +42 -1
  77. package/scripts/published-cli.js +0 -8
  78. package/scripts/rate-limiter.js +64 -13
  79. package/scripts/secret-scanner.js +44 -5
  80. package/scripts/security-scanner.js +260 -10
  81. package/scripts/self-distill-agent.js +3 -1
  82. package/scripts/seo-gsd.js +916 -7
  83. package/scripts/statusline-cache-path.js +17 -2
  84. package/scripts/statusline-local-stats.js +9 -1
  85. package/scripts/statusline-meta.js +28 -2
  86. package/scripts/statusline.sh +20 -4
  87. package/scripts/telemetry-analytics.js +357 -0
  88. package/scripts/thompson-sampling.js +31 -10
  89. package/scripts/thumbgate-bench.js +16 -1
  90. package/scripts/thumbgate-search.js +85 -19
  91. package/scripts/tool-registry.js +169 -1
  92. package/scripts/vector-store.js +45 -0
  93. package/scripts/workflow-sentinel.js +286 -53
  94. package/scripts/workspace-evolver.js +62 -2
  95. package/src/api/server.js +2683 -319
  96. package/scripts/bot-detector.js +0 -50
@@ -5,12 +5,13 @@
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-production.up.railway.app/dashboard">
8
+ <link rel="canonical" href="https://thumbgate.ai/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">
12
13
  <!-- Privacy-friendly analytics by Plausible -->
13
- <script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
14
+ <script defer data-domain="thumbgate.ai" src="https://plausible.io/js/script.js"></script>
14
15
  <script type="application/ld+json">
15
16
  {
16
17
  "@context": "https://schema.org",
@@ -18,7 +19,7 @@
18
19
  "name": "ThumbGate Dashboard",
19
20
  "applicationCategory": "DeveloperApplication",
20
21
  "operatingSystem": "Cross-platform",
21
- "url": "https://thumbgate-production.up.railway.app/dashboard",
22
+ "url": "https://thumbgate.ai/dashboard",
22
23
  "dateModified": "2026-04-20",
23
24
  "creator": {
24
25
  "@type": "Person",
@@ -102,7 +103,7 @@
102
103
  .result-tags { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
103
104
  .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; }
104
105
  .tag:hover { background: rgba(34,211,238,0.25); transform: translateY(-1px); }
105
- mark { background: rgba(34,211,238,0.25); color: var(--cyan); border-radius: 2px; padding: 0 2px; }
106
+ mark { background: #fbbf24; color: #000000; font-weight: bold; border-radius: 2px; padding: 0 3px; }
106
107
 
107
108
  /* GATES */
108
109
  .gates-section { margin-bottom: 32px; }
@@ -178,6 +179,74 @@
178
179
  .demo-banner a { color: var(--cyan); text-decoration: none; font-weight: 600; }
179
180
  .demo-banner a:hover { text-decoration: underline; }
180
181
 
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
+
181
250
  .empty { text-align: center; padding: 48px; color: var(--text-muted); font-size: 15px; }
182
251
  .loading { text-align: center; padding: 24px; color: var(--text-muted); }
183
252
  h2 { font-size: 20px; font-weight: 700; margin-bottom: 16px; letter-spacing: -0.02em; }
@@ -189,11 +258,31 @@
189
258
  .settings-card .team-value, .origin-value { font-size: 18px; font-weight: 700; color: var(--text); margin-top: 8px; word-break: break-word; }
190
259
  .origin-list, .layer-list, .routing-list { display: flex; flex-direction: column; gap: 12px; }
191
260
  .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
+ }
192
281
 
193
282
  @media (max-width: 700px) {
194
283
  .stats-grid { grid-template-columns: repeat(2, 1fr); }
195
284
  .search-filters { flex-wrap: wrap; }
196
- .team-grid, .template-grid, .team-columns, .settings-grid, .generated-grid, .inventory-grid { grid-template-columns: 1fr; }
285
+ .team-grid, .template-grid, .team-columns, .settings-grid, .generated-grid, .inventory-grid, .enterprise-chat-layout { grid-template-columns: 1fr; }
197
286
  }
198
287
  </style>
199
288
  </head>
@@ -201,7 +290,7 @@
201
290
 
202
291
  <nav>
203
292
  <div class="container">
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>
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>
205
294
  <div class="nav-links">
206
295
  <a href="/dashboard" class="active" style="color:var(--cyan);">Dashboard</a>
207
296
  <a href="/lessons">Lessons</a>
@@ -214,13 +303,14 @@
214
303
  <div class="auth-bar">
215
304
  <div class="container">
216
305
  <div style="margin-bottom:10px;">
217
- <span style="font-size:14px;font-weight:600;">Connect your API key</span>
306
+ <span id="authTitle" style="font-size:14px;font-weight:600;">Connect your API key</span>
218
307
  <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>
219
308
  </div>
220
309
  <div style="display:flex;gap:12px;align-items:center;">
221
310
  <input type="password" class="auth-input" id="apiKey" placeholder="Paste your API key here (THUMBGATE_API_KEY or THUMBGATE_PRO_KEY)..." />
222
311
  <button class="btn" id="connectBtn" onclick="connect()">Connect</button>
223
312
  <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>
224
314
  <span class="auth-status" id="authStatus"></span>
225
315
  </div>
226
316
  </div>
@@ -233,10 +323,10 @@
233
323
  <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>
234
324
  <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>
235
325
  <div style="display:flex;gap:16px;margin-top:12px;font-size:12px;color:var(--text-muted);">
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>
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>
240
330
  </div>
241
331
  </div>
242
332
 
@@ -247,16 +337,33 @@
247
337
 
248
338
  <!-- STATS -->
249
339
  <div class="stats-grid" id="statsGrid">
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>
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>
254
361
  </div>
255
362
 
256
363
  <div class="panel" id="reviewDeltaPanel" style="margin-bottom:20px;">
257
364
  <div style="display:flex;justify-content:space-between;gap:16px;align-items:flex-start;flex-wrap:wrap;">
258
365
  <div>
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>
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>
260
367
  <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>
261
368
  </div>
262
369
  <button class="btn-outline" id="reviewCheckpointBtn" onclick="markReviewed()">Mark Current Dashboard Reviewed</button>
@@ -284,6 +391,8 @@
284
391
  <div class="tab active" onclick="switchTab('search')">๐Ÿ” Search Memories</div>
285
392
  <div class="tab" onclick="switchTab('gates')">๐Ÿ›ก๏ธ Active Gates</div>
286
393
  <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>
287
396
  <div class="tab" onclick="switchTab('generated')">๐Ÿงฉ Generated Views</div>
288
397
  <div class="tab" onclick="switchTab('settings')">โš™๏ธ Policy Origins</div>
289
398
  <div class="tab" onclick="switchTab('templates')">๐Ÿงฑ Gate Templates</div>
@@ -293,6 +402,12 @@
293
402
 
294
403
  <!-- SEARCH TAB -->
295
404
  <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>
296
411
  <div class="search-section">
297
412
  <div class="search-bar">
298
413
  <input type="text" class="search-input" id="searchQuery" placeholder="Search memories... (try: git, test, deploy, secrets, database)" onkeydown="if(event.key==='Enter')search()" />
@@ -314,6 +429,74 @@
314
429
  <div class="tab-content" id="tab-gates">
315
430
  <div class="gates-section">
316
431
  <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
+
317
500
  <div id="gatesList"><div class="loading">Loading gates...</div></div>
318
501
  </div>
319
502
  </div>
@@ -387,6 +570,56 @@
387
570
  </div>
388
571
  </div>
389
572
 
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
+
390
623
  <!-- GENERATED TAB -->
391
624
  <div class="tab-content" id="tab-generated">
392
625
  <div class="templates-section">
@@ -460,13 +693,7 @@
460
693
  </div>
461
694
 
462
695
  <!-- Charts -->
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>
696
+ <div style="margin-bottom:24px;">
470
697
  <div class="panel">
471
698
  <h3>Lessons Generated (30 days)</h3>
472
699
  <div style="position:relative;height:260px;margin-top:12px;">
@@ -539,6 +766,91 @@
539
766
  </div>
540
767
 
541
768
  <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
+
542
854
  let API_KEY = '';
543
855
  let currentSource = 'all';
544
856
  let isDemo = false;
@@ -548,38 +860,237 @@ let currentGeneratedView = 'team-review';
548
860
  const BOOTSTRAP_API_KEY = __DASHBOARD_BOOTSTRAP_KEY__;
549
861
  const LOCAL_PRO_BOOTSTRAP = __DASHBOARD_BOOTSTRAP_ENABLED__;
550
862
 
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
+
551
881
  function getHeaders() {
552
- return { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' };
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
+ }
553
996
  }
554
997
 
555
998
  function hasBootstrapKey() {
556
999
  return LOCAL_PRO_BOOTSTRAP && Boolean(BOOTSTRAP_API_KEY);
557
1000
  }
558
1001
 
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
+
559
1033
  async function connect(options) {
560
1034
  var opts = options || {};
561
1035
  var input = document.getElementById('apiKey');
562
1036
  API_KEY = String(opts.key || input.value || '').trim();
563
1037
  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
+
564
1042
  const status = document.getElementById('authStatus');
565
1043
  const btn = document.getElementById('connectBtn');
566
1044
  btn.disabled = true;
567
1045
  status.className = 'auth-status';
568
- status.textContent = opts.localPro ? 'Connecting local dashboard...' : 'Connecting...';
1046
+ status.textContent = opts.localPro ? `Connecting local dashboard...` : 'Connecting...';
569
1047
  try {
570
1048
  const res = await fetch('/v1/feedback/stats', { headers: getHeaders() });
571
1049
  if (!res.ok) throw new Error('Invalid API key');
572
1050
  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
+
573
1077
  status.className = 'auth-status ok';
574
- status.textContent = opts.localPro ? 'โœ“ Local Pro dashboard connected' : 'โœ“ Connected';
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
+ }
575
1084
  document.getElementById('dashboardContent').style.display = 'block';
576
1085
  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.';
577
1088
  input.value = 'local-license';
578
1089
  input.disabled = true;
579
- input.placeholder = 'Local Pro auto-connected';
1090
+ input.placeholder = `Local ${tierName} auto-connected`;
580
1091
  btn.disabled = true;
581
1092
  document.getElementById('demoBtn').style.display = 'none';
582
- document.getElementById('authHelp').textContent = 'Local Pro is active on this machine. Your personal dashboard is using the saved license key automatically.';
1093
+ document.getElementById('authHelp').textContent = isEnterprise ? localEnterpriseActiveMessage : localProActiveMessage;
583
1094
  }
584
1095
  renderStats(data);
585
1096
  setSelectedCard('all');
@@ -591,8 +1102,12 @@ async function connect(options) {
591
1102
  } catch (e) {
592
1103
  status.className = 'auth-status err';
593
1104
  status.textContent = 'โœ— ' + e.message;
1105
+ const titleEl = document.getElementById('authTitle');
1106
+ if (titleEl) {
1107
+ titleEl.textContent = 'Connect your API key';
1108
+ }
594
1109
  if (opts.localPro) {
595
- document.getElementById('authHelp').textContent = 'Local Pro bootstrap failed. Paste your THUMBGATE_API_KEY manually or retry the local launcher.';
1110
+ document.getElementById('authHelp').textContent = `Local ${tierName} bootstrap failed. Paste your THUMBGATE_API_KEY manually or retry the local launcher.`;
596
1111
  }
597
1112
  }
598
1113
  if (!opts.localPro) {
@@ -656,24 +1171,88 @@ async function search() {
656
1171
  }
657
1172
 
658
1173
  function renderResult(r) {
659
- const signal = r.signal || r.type || '';
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
+
660
1229
  const isUp = signal === 'up' || signal === 'positive' || signal === 'thumbs_up';
661
1230
  const isDown = signal === 'down' || signal === 'negative' || signal === 'thumbs_down';
662
1231
  const signalClass = isUp ? 'up' : isDown ? 'down' : '';
663
1232
  const signalLabel = isUp ? '๐Ÿ‘ Positive' : isDown ? '๐Ÿ‘Ž Negative' : signal;
664
- const title = r.title || r.context || r.text || '';
665
- const context = r.context || r.text || r.content || '';
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
+
666
1244
  const tags = r.tags || [];
667
1245
  const date = r.timestamp ? new Date(r.timestamp).toLocaleDateString() : '';
1246
+
668
1247
  return '<div class="result-card">' +
669
1248
  '<div class="result-header">' +
670
1249
  (signalLabel ? '<span class="result-signal ' + signalClass + '">' + signalLabel + '</span>' : '<span></span>') +
671
1250
  '<span class="result-date">' + date + '</span>' +
672
1251
  '</div>' +
673
- (title ? '<div class="result-title">' + highlightText(title) + '</div>' : '') +
674
- (context && context !== title ? '<div class="result-context">' + highlightText(context.slice(0, 300)) + '</div>' : '') +
1252
+ (displayTitle ? '<div class="result-title">' + highlightText(displayTitle) + '</div>' : '') +
1253
+ (contextText && contextText !== displayTitle ? '<div class="result-context">' + highlightText(contextText.slice(0, 300)) + '</div>' : '') +
675
1254
  (tags.length ? '<div class="result-tags">' + tags.map(function(t) {
676
- return '<button type="button" class="tag" data-tag="' + encodeURIComponent(String(t)) + '" title="' + escAttr('Search for ' + t) + '">' + escHtml(t) + '</button>';
1255
+ return '<button type="button" class="tag" data-tag="' + encodeURIComponent(String(t)) + '" title="' + escAttr('Search for ' + t) + '">' + highlightText(t) + '</button>';
677
1256
  }).join('') + '</div>' : '') +
678
1257
  '</div>';
679
1258
  }
@@ -750,10 +1329,23 @@ function setSource(el, source) {
750
1329
  function switchTab(name) {
751
1330
  document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
752
1331
  document.querySelectorAll('.tab-content').forEach(function(c) { c.classList.remove('active'); });
753
- var tabEl = document.querySelector('[onclick*="' + name + '"]');
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 + '"]');
754
1338
  var contentEl = document.getElementById('tab-' + name);
755
1339
  if (tabEl) tabEl.classList.add('active');
756
- if (contentEl) contentEl.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
+ }
757
1349
  // Sync URL hash so deep-links stay shareable without scroll jump
758
1350
  try {
759
1351
  if (('#' + name) !== window.location.hash) {
@@ -765,11 +1357,11 @@ function switchTab(name) {
765
1357
  /**
766
1358
  * Resolve deep-link tab target from URL hash or query string.
767
1359
  * Supports: /dashboard#insights, /dashboard?tab=gates, /dashboard#tab-export.
768
- * Valid targets match tab-content ids (search, gates, team, generated,
769
- * settings, templates, insights, export).
1360
+ * Valid targets match tab-content ids (search, gates, team, enterprise,
1361
+ * ai-inventory, generated, settings, templates, insights, export).
770
1362
  */
771
1363
  function getDeepLinkTab() {
772
- var valid = ['search', 'gates', 'team', 'generated', 'settings', 'templates', 'insights', 'export'];
1364
+ var valid = ['search', 'gates', 'team', 'enterprise', 'ai-inventory', 'generated', 'settings', 'templates', 'insights', 'export'];
773
1365
  var raw = (window.location.hash || '').replace(/^#/, '').replace(/^tab-/, '');
774
1366
  if (!raw) {
775
1367
  try {
@@ -936,9 +1528,14 @@ async function markReviewed() {
936
1528
  try {
937
1529
  var res = await fetch('/v1/dashboard/review-state', {
938
1530
  method: 'POST',
939
- headers: getHeaders()
1531
+ headers: getHeaders(),
1532
+ body: JSON.stringify({ reviewedAt: new Date().toISOString() })
940
1533
  });
941
- if (!res.ok) throw new Error('Failed to save review checkpoint');
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
+ }
942
1539
  var body = await res.json();
943
1540
  renderReviewDelta(body.reviewDelta || {});
944
1541
  } catch (e) {
@@ -976,6 +1573,68 @@ function renderDashboardData(data) {
976
1573
  renderRegulatedProof(data.regulatedProof || {});
977
1574
  renderTemplates(data.templateLibrary || {});
978
1575
  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
+ }
979
1638
  }
980
1639
 
981
1640
  function renderGeneratedViewToolbar(spec) {
@@ -1415,6 +2074,108 @@ function renderTemplates(templateLibrary) {
1415
2074
  : '<div class="empty">No check templates available</div>';
1416
2075
  }
1417
2076
 
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
+
1418
2179
  document.addEventListener('click', function(event) {
1419
2180
  var tagButton = event.target.closest('.tag[data-tag]');
1420
2181
  if (!tagButton) return;
@@ -1428,71 +2189,65 @@ function loadDemo() {
1428
2189
  // Demo badge in nav
1429
2190
  var logo = document.querySelector('.nav-logo');
1430
2191
  if (!document.getElementById('demoBadge')) {
1431
- logo.insertAdjacentHTML('afterend', '<span class="demo-badge" id="demoBadge">Demo Mode</span>');
2192
+ logo.insertAdjacentHTML('afterend', '<span class="demo-badge" id="demoBadge">Demo Mode โ€” Jamie M., Partner ยท Litigation Intake</span>');
1432
2193
  }
1433
2194
  // Stats
1434
- document.getElementById('statTotal').textContent = '297';
1435
- document.getElementById('statPositive').textContent = '37';
1436
- document.getElementById('statNegative').textContent = '260';
1437
- document.getElementById('statGates').textContent = '21';
2195
+ document.getElementById('statTotal').textContent = '184';
2196
+ document.getElementById('statPositive').textContent = '91';
2197
+ document.getElementById('statNegative').textContent = '93';
2198
+ document.getElementById('statGates').textContent = '14';
1438
2199
  setSelectedCard('all');
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);
2200
+ var demoSavings = TG_TOKEN_SAVINGS.compute(93, 42);
1443
2201
  var tEl = document.getElementById('statTokensSaved');
1444
2202
  var dEl = document.getElementById('statDollarsSaved');
1445
2203
  if (tEl) tEl.textContent = demoSavings.tokensSavedDisplay;
1446
2204
  if (dEl) dEl.textContent = 'โ‰ˆ ' + demoSavings.dollarsSavedDisplay + ' saved';
1447
2205
  renderTopBlockedGates([
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 },
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 },
1454
2212
  ]);
1455
2213
  renderReviewDelta({
1456
2214
  hasBaseline: true,
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.',
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.',
1466
2224
  latestFeedback: {
1467
- title: 'Claimed fix worked without running tests',
1468
- timestamp: '2025-06-22T10:20:00Z'
2225
+ title: 'Intake agent provided jurisdictional recommendation without attorney review',
2226
+ timestamp: '2026-05-25T14:20:00Z'
1469
2227
  },
1470
2228
  latestLesson: {
1471
- title: 'MISTAKE: Claimed Figma compliance without visual verification',
1472
- timestamp: '2025-06-21T08:10:00Z'
2229
+ title: 'BLOCKED: Advice-shaped response detected โ€” routed to attorney review queue',
2230
+ timestamp: '2026-05-24T11:10:00Z'
1473
2231
  }
1474
2232
  });
1475
2233
  document.getElementById('reviewCheckpointBtn').disabled = true;
1476
2234
  document.getElementById('reviewCheckpointBtn').textContent = 'Connect to save your own review checkpoint';
1477
- // Sample memories โ€” realistic scenarios from real agent-driven development
2235
+ // Sample memories โ€” legal AI intake scenarios for law firm pilot demo
1478
2236
  var demoResults = [
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' }
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' }
1496
2251
  ];
1497
2252
  isDemo = true;
1498
2253
  demoData = demoResults;
@@ -1506,14 +2261,14 @@ function loadDemo() {
1506
2261
  '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>' +
1507
2262
  '</div>';
1508
2263
  document.getElementById('searchResults').innerHTML = upgradeBanner + teaserHtml;
1509
- // Sample gates
2264
+ // Sample gates โ€” legal AI intake enforcement
1510
2265
  var demoGates = [
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' }
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' }
1517
2272
  ];
1518
2273
  document.getElementById('gatesList').innerHTML = demoGates.map(function(g) {
1519
2274
  return '<div class="gate-card"><div><div class="gate-name">' + escHtml(g.name) + '</div>' +
@@ -1521,141 +2276,144 @@ function loadDemo() {
1521
2276
  '<span class="gate-action ' + g.action + '">' + g.action.toUpperCase() + '</span></div>';
1522
2277
  }).join('');
1523
2278
  renderTeam({
1524
- totalAgents: 8,
1525
- activeAgents: 5,
2279
+ totalAgents: 4,
2280
+ activeAgents: 3,
1526
2281
  windowHours: 24,
1527
- orgAdherenceRate: 92.4,
2282
+ orgAdherenceRate: 96.1,
1528
2283
  riskAgents: [
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 }
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 }
1531
2286
  ],
1532
2287
  topBlockedGates: [
1533
- { gateId: 'evidence-before-done', blocked: 12, warned: 1 },
1534
- { gateId: 'never-force-push-main', blocked: 7, warned: 0 }
2288
+ { gateId: 'block-unauthorized-practice', blocked: 34, warned: 0 },
2289
+ { gateId: 'require-conflict-clearance', blocked: 22, warned: 3 }
1535
2290
  ]
1536
2291
  }, {
1537
2292
  northStar: {
1538
- weeklyTeamsRunningProofBackedWorkflows: 3,
2293
+ weeklyTeamsRunningProofBackedWorkflows: 2,
1539
2294
  paidTeamRuns: 1
1540
2295
  }
1541
2296
  });
1542
2297
  renderPredictive({
1543
2298
  upgradePropensity: {
1544
- pro: { band: 'high', score: 0.71 },
1545
- team: { band: 'medium', score: 0.54 }
2299
+ pro: { band: 'high', score: 0.84 },
2300
+ team: { band: 'high', score: 0.72 }
1546
2301
  },
1547
2302
  revenueForecast: {
1548
- predictedBookedRevenueCents: 12800,
1549
- incrementalOpportunityCents: 4900
2303
+ predictedBookedRevenueCents: 8400000,
2304
+ incrementalOpportunityCents: 3200000
1550
2305
  },
1551
2306
  anomalySummary: {
1552
2307
  count: 2,
1553
2308
  severity: 'warning'
1554
2309
  },
1555
2310
  topCreators: [
1556
- { key: 'reach_vb', opportunityRevenueCents: 3100 }
2311
+ { key: 'innovation_team', opportunityRevenueCents: 4800000 }
1557
2312
  ],
1558
2313
  topSources: [
1559
- { key: 'producthunt', opportunityRevenueCents: 1800 }
2314
+ { key: 'direct_outreach', opportunityRevenueCents: 2400000 }
1560
2315
  ],
1561
2316
  anomalies: [
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' }
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' }
1564
2319
  ]
1565
2320
  });
1566
2321
  renderAgentInventory({
1567
- profile: 'default',
1568
- tier: 'builder',
1569
- configuredServerCount: 3,
1570
- observedToolCount: 4,
2322
+ profile: 'legal-intake',
2323
+ tier: 'enterprise',
2324
+ configuredServerCount: 2,
2325
+ observedToolCount: 5,
1571
2326
  observedTools: [
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 }
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 }
1575
2332
  ],
1576
2333
  policySources: [
1577
- { source: 'gates-engine', count: 23 },
1578
- { source: 'secret-guard', count: 6 },
1579
- { source: 'workflow-sentinel', count: 4 }
2334
+ { source: 'legal-intake-rules', count: 14 },
2335
+ { source: 'privilege-guard', count: 6 },
2336
+ { source: 'conflict-sentinel', count: 4 }
1580
2337
  ]
1581
2338
  });
1582
2339
  renderActionableRemediations([
1583
2340
  {
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',
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',
1587
2344
  },
1588
2345
  {
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',
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',
1592
2349
  },
1593
2350
  {
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',
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',
1597
2354
  }
1598
2355
  ]);
1599
2356
  renderSettingsStatus({
1600
2357
  activeLayers: [
1601
2358
  { scope: 'defaults', exists: true, leafCount: 7, sourcePath: null },
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' }
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' }
1606
2363
  ],
1607
2364
  resolvedSettings: {
1608
- mcp: { defaultProfile: 'essential', readonlySessionProfile: 'readonly' },
2365
+ intake: { defaultProfile: 'legal-intake', enforcementMode: 'strict' },
1609
2366
  harnesses: { enabled: true, allowRuntimeExecution: true }
1610
2367
  },
1611
2368
  routingPreview: {
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' }
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' }
1615
2372
  },
1616
2373
  origins: [
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' }
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' }
1621
2378
  ]
1622
2379
  });
1623
2380
  renderTemplates({
1624
2381
  total: 6,
1625
2382
  categories: {
1626
- 'Git Safety': 1,
1627
- 'Verification': 1,
1628
- 'Agent Honesty': 1,
1629
- 'Database Safety': 1,
1630
- 'Secrets Hygiene': 1,
1631
- 'Positive Reinforcement': 1
2383
+ 'UPL Prevention': 2,
2384
+ 'Conflict Detection': 1,
2385
+ 'Privilege Protection': 1,
2386
+ 'Model Governance': 1,
2387
+ 'Intake Routing': 1
1632
2388
  },
1633
2389
  templates: [
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' }
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)' }
1638
2396
  ]
1639
2397
  });
1640
2398
  renderGeneratedView(buildDemoGeneratedViewSpec(currentGeneratedView));
1641
2399
  renderInsights({
1642
- gateStats: { blocked: 180 },
2400
+ gateStats: { blocked: 93 },
1643
2401
  tokenSavings: {
1644
2402
  dollarsSaved: 0,
1645
2403
  dollarsSavedDisplay: '$0.00',
1646
2404
  tokensSavedDisplay: '0',
1647
2405
  blockedCalls: 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 }
2406
+ modelMix: { 'gpt-4o': 0.5, 'claude-sonnet-4.5': 0.35, 'azure-openai': 0.15 },
2407
+ blendedPricePer1M: { input: 5.0, output: 15.0 }
1650
2408
  },
1651
2409
  lessonPipeline: {
1652
2410
  stages: [
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' }
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' }
1657
2415
  ],
1658
- rates: { feedbackToLesson: 32, lessonToGate: 22 }
2416
+ rates: { feedbackToLesson: 23, lessonToGate: 33 }
1659
2417
  },
1660
2418
  feedbackTimeSeries: {
1661
2419
  days: Array.from({ length: 30 }, function(_, index) {
@@ -1679,14 +2437,14 @@ function loadDemo() {
1679
2437
  },
1680
2438
  actionableRemediations: [
1681
2439
  {
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'
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'
1685
2443
  },
1686
2444
  {
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'
2445
+ title: 'extend conflict checks to affiliates',
2446
+ rationale: 'Named-party fixture misses affiliates and parents.',
2447
+ action: 'extend-conflict-coverage'
1690
2448
  }
1691
2449
  ]
1692
2450
  });
@@ -1694,6 +2452,7 @@ function loadDemo() {
1694
2452
 
1695
2453
  // Auto-load demo on first visit so visitors see the product immediately
1696
2454
  window.addEventListener('DOMContentLoaded', function() {
2455
+ preserveProjectQueryParams();
1697
2456
  if (hasBootstrapKey()) {
1698
2457
  connect({ key: BOOTSTRAP_API_KEY, localPro: true });
1699
2458
  return;
@@ -1848,6 +2607,7 @@ function chartDefaults() {
1848
2607
  function renderFeedbackTrendChart(ts) {
1849
2608
  var canvas = document.getElementById('feedbackTrendChart');
1850
2609
  if (!canvas) return;
2610
+ if (typeof Chart === 'undefined') return;
1851
2611
  var days = ts.days || [];
1852
2612
  if (feedbackChart) feedbackChart.destroy();
1853
2613
 
@@ -1898,6 +2658,7 @@ function renderFeedbackTrendChart(ts) {
1898
2658
  function renderLessonTrendChart(ts) {
1899
2659
  var canvas = document.getElementById('lessonTrendChart');
1900
2660
  if (!canvas) return;
2661
+ if (typeof Chart === 'undefined') return;
1901
2662
  var days = ts.days || [];
1902
2663
  if (lessonChart) lessonChart.destroy();
1903
2664
 
@@ -1956,6 +2717,7 @@ function renderLessonTrendChart(ts) {
1956
2717
  function renderGateAuditChartFromData(gateAudit) {
1957
2718
  var canvas = document.getElementById('gateAuditChart');
1958
2719
  if (!canvas) return;
2720
+ if (typeof Chart === 'undefined') return;
1959
2721
  var days = gateAudit.days || [];
1960
2722
  if (gateAuditChart) gateAuditChart.destroy();
1961
2723
 
@@ -1999,6 +2761,8 @@ function renderGateAuditChartFromData(gateAudit) {
1999
2761
  },
2000
2762
  });
2001
2763
  }
2764
+
2002
2765
  </script>
2766
+ <script src="/js/buyer-intent.js"></script>
2003
2767
  </body>
2004
2768
  </html>