thumbgate 1.4.5 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,989 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ThumbGate — Lessons Learned</title>
7
+ <script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
8
+ <style>
9
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
10
+ :root {
11
+ --bg: #0a0a0b; --bg-raised: #111113; --bg-card: #161618; --border: #222225;
12
+ --text: #e8e8ec; --text-muted: #8b8b96; --cyan: #22d3ee;
13
+ --cyan-dim: rgba(34,211,238,0.12); --green: #4ade80; --red: #f87171;
14
+ --yellow: #fbbf24; --purple: #a78bfa;
15
+ --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto, sans-serif;
16
+ --mono: 'SF Mono', 'Cascadia Code', 'JetBrains Mono', 'Fira Code', Consolas, monospace;
17
+ }
18
+ html { scroll-behavior: smooth; }
19
+ body { font-family: var(--font); background: var(--bg); color: var(--text); line-height: 1.6; -webkit-font-smoothing: antialiased; }
20
+ .container { max-width: 1060px; margin: 0 auto; padding: 0 24px; }
21
+
22
+ /* NAV */
23
+ nav { position: sticky; top: 0; z-index: 50; background: rgba(10,10,11,0.85); backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); padding: 14px 0; }
24
+ nav .container { display: flex; justify-content: space-between; align-items: center; }
25
+ .nav-logo { font-weight: 700; font-size: 15px; color: var(--text); text-decoration: none; }
26
+ .nav-links { display: flex; gap: 16px; align-items: center; }
27
+ .nav-links a { color: var(--text-muted); text-decoration: none; font-size: 13px; }
28
+ .nav-links a:hover { color: var(--text); }
29
+ .nav-links a.active { color: var(--cyan); }
30
+
31
+ /* STATS */
32
+ .stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin: 32px 0; }
33
+ .stat-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 20px; transition: border-color 0.15s, transform 0.1s; cursor: pointer; }
34
+ .stat-card:hover { border-color: rgba(34,211,238,0.4); transform: translateY(-2px); }
35
+ .stat-card:active { transform: translateY(0); border-color: rgba(34,211,238,0.7); }
36
+ .stat-card.selected { border-color: rgba(34,211,238,0.6); background: rgba(34,211,238,0.05); }
37
+ .stat-label { font-size: 12px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 6px; }
38
+ .stat-value { font-size: 28px; font-weight: 700; letter-spacing: -0.02em; }
39
+ .stat-value.green { color: var(--green); }
40
+ .stat-value.red { color: var(--red); }
41
+ .stat-value.cyan { color: var(--cyan); }
42
+ .stat-value.purple { color: var(--purple); }
43
+ .stat-sub { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
44
+
45
+ /* IMPROVEMENT METRICS */
46
+ .metrics-panel { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 22px; margin: 0 0 24px; }
47
+ .metrics-header { display: flex; justify-content: space-between; gap: 16px; align-items: flex-start; margin-bottom: 18px; }
48
+ .metrics-header h2 { margin-bottom: 6px; }
49
+ .metrics-header p { font-size: 13px; color: var(--text-muted); max-width: 720px; }
50
+ .metrics-summary-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 18px; }
51
+ .metric-tile { background: var(--bg-raised); border: 1px solid var(--border); border-radius: 10px; padding: 16px; min-height: 94px; }
52
+ .metric-kicker { font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); margin-bottom: 6px; }
53
+ .metric-number { font-size: 24px; font-weight: 700; letter-spacing: -0.02em; }
54
+ .metric-number.green { color: var(--green); }
55
+ .metric-number.red { color: var(--red); }
56
+ .metric-number.cyan { color: var(--cyan); }
57
+ .metric-number.purple { color: var(--purple); }
58
+ .metric-note { font-size: 12px; color: var(--text-muted); margin-top: 6px; }
59
+ .metrics-chart-wrap { background: var(--bg-raised); border: 1px solid var(--border); border-radius: 10px; padding: 16px; }
60
+ .metrics-chart-title { display: flex; justify-content: space-between; gap: 12px; align-items: center; margin-bottom: 12px; }
61
+ .metrics-chart-title h3 { font-size: 14px; margin: 0; }
62
+ .metrics-chart-title p { font-size: 12px; color: var(--text-muted); }
63
+ .metrics-legend { display: flex; gap: 12px; flex-wrap: wrap; font-size: 12px; color: var(--text-muted); margin-bottom: 12px; }
64
+ .metrics-legend span { display: inline-flex; align-items: center; gap: 6px; }
65
+ .metrics-legend-dot { width: 8px; height: 8px; border-radius: 999px; display: inline-block; }
66
+ .metrics-legend-dot.up { background: var(--green); }
67
+ .metrics-legend-dot.down { background: var(--red); }
68
+ .metrics-legend-dot.deny { background: var(--cyan); }
69
+ .metrics-legend-dot.warn { background: var(--yellow); }
70
+ .metrics-chart { display: grid; grid-template-columns: repeat(14, minmax(0, 1fr)); gap: 8px; align-items: end; min-height: 180px; }
71
+ .metrics-bar { background: transparent; border: 1px solid transparent; border-radius: 10px; padding: 8px 4px; color: inherit; cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 8px; min-width: 0; }
72
+ .metrics-bar:hover { border-color: rgba(34,211,238,0.35); background: rgba(34,211,238,0.04); }
73
+ .metrics-bar.selected { border-color: rgba(34,211,238,0.65); background: rgba(34,211,238,0.08); }
74
+ .metrics-bar-body { width: 100%; display: flex; justify-content: center; align-items: flex-end; gap: 6px; }
75
+ .metrics-bar-stack { width: 100%; max-width: 28px; height: 120px; display: flex; flex-direction: column; justify-content: flex-end; gap: 3px; }
76
+ .metrics-bar-stack.gate { max-width: 16px; height: 84px; }
77
+ .metrics-bar-empty { width: 100%; border-radius: 8px; background: rgba(255,255,255,0.04); border: 1px dashed rgba(255,255,255,0.05); flex: 1; }
78
+ .metrics-segment { width: 100%; border-radius: 8px; min-height: 0; }
79
+ .metrics-segment.up { background: linear-gradient(180deg, rgba(74,222,128,0.95), rgba(74,222,128,0.55)); }
80
+ .metrics-segment.down { background: linear-gradient(180deg, rgba(248,113,113,0.95), rgba(248,113,113,0.55)); }
81
+ .metrics-segment.deny { background: linear-gradient(180deg, rgba(34,211,238,0.95), rgba(34,211,238,0.55)); }
82
+ .metrics-segment.warn { background: linear-gradient(180deg, rgba(251,191,36,0.95), rgba(251,191,36,0.55)); }
83
+ .metrics-bar-total { font-size: 12px; font-weight: 600; color: var(--text); }
84
+ .metrics-bar-subtotal { font-size: 10px; color: var(--text-muted); line-height: 1; }
85
+ .metrics-bar-label { font-size: 11px; color: var(--text-muted); white-space: nowrap; }
86
+ .metrics-chart-note { font-size: 12px; color: var(--text-muted); margin-top: 12px; }
87
+
88
+ /* TABS */
89
+ .tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 24px; }
90
+ .tab { padding: 12px 20px; font-size: 14px; font-weight: 500; color: var(--text-muted); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; }
91
+ .tab:hover { color: var(--text); }
92
+ .tab.active { color: var(--cyan); border-bottom-color: var(--cyan); }
93
+ .tab-content { display: none; }
94
+ .tab-content.active { display: block; }
95
+
96
+ /* SEARCH */
97
+ .search-bar { display: flex; gap: 12px; margin-bottom: 16px; }
98
+ .search-input { flex: 1; background: var(--bg-raised); border: 1px solid var(--border); border-radius: 8px; padding: 12px 16px; color: var(--text); font-size: 15px; }
99
+ .search-input:focus { outline: none; border-color: var(--cyan); }
100
+ .search-input::placeholder { color: var(--text-muted); }
101
+ .btn { background: var(--cyan); color: var(--bg); padding: 10px 20px; border: none; border-radius: 8px; font-weight: 600; font-size: 13px; cursor: pointer; }
102
+ .btn:hover { opacity: 0.85; }
103
+ .filter-row { display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap; }
104
+ .filter-btn { background: var(--bg-raised); border: 1px solid var(--border); border-radius: 6px; padding: 6px 14px; color: var(--text-muted); font-size: 12px; cursor: pointer; }
105
+ .filter-btn:hover, .filter-btn.active { border-color: var(--cyan); color: var(--cyan); }
106
+
107
+ /* RULE CARDS */
108
+ .rules-list { display: flex; flex-direction: column; gap: 12px; }
109
+ .rule-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px; padding: 20px; transition: border-color 0.15s; }
110
+ .rule-card:hover { border-color: rgba(34,211,238,0.3); }
111
+ .rule-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px; gap: 12px; }
112
+ .rule-title { font-size: 15px; font-weight: 600; }
113
+ .rule-severity { font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 4px; text-transform: uppercase; letter-spacing: 0.05em; }
114
+ .rule-severity.critical { background: rgba(248,113,113,0.15); color: var(--red); }
115
+ .rule-severity.warning { background: rgba(251,191,36,0.15); color: var(--yellow); }
116
+ .rule-severity.info { background: var(--cyan-dim); color: var(--cyan); }
117
+ .rule-context { font-size: 13px; color: var(--text-muted); line-height: 1.5; margin-bottom: 10px; }
118
+ .rule-meta { display: flex; gap: 16px; flex-wrap: wrap; align-items: center; }
119
+ .rule-meta-item { font-size: 12px; color: var(--text-muted); display: flex; align-items: center; gap: 4px; }
120
+ .rule-meta-item .value { color: var(--text); font-weight: 600; }
121
+ .rule-meta-item .value.green { color: var(--green); }
122
+ .rule-effectiveness { font-size: 12px; font-weight: 600; color: var(--green); background: rgba(74,222,128,0.1); padding: 2px 8px; border-radius: 4px; }
123
+ .rule-tags { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
124
+ .tag { font-size: 11px; background: var(--cyan-dim); color: var(--cyan); padding: 2px 8px; border-radius: 4px; }
125
+
126
+ /* TIMELINE */
127
+ .timeline { position: relative; padding-left: 24px; }
128
+ .timeline::before { content: ''; position: absolute; left: 8px; top: 0; bottom: 0; width: 2px; background: var(--border); }
129
+ .timeline-item { position: relative; margin-bottom: 20px; }
130
+ .timeline-dot { position: absolute; left: -20px; top: 6px; width: 12px; height: 12px; border-radius: 50%; border: 2px solid var(--bg); }
131
+ .timeline-dot.up { background: var(--green); }
132
+ .timeline-dot.down { background: var(--red); }
133
+ .timeline-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px; padding: 16px; }
134
+ .timeline-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
135
+ .timeline-signal { font-size: 13px; font-weight: 600; }
136
+ .timeline-signal.up { color: var(--green); }
137
+ .timeline-signal.down { color: var(--red); }
138
+ .timeline-date { font-size: 12px; color: var(--text-muted); }
139
+ .timeline-context { font-size: 13px; color: var(--text-muted); }
140
+ .timeline-learned { font-size: 12px; color: var(--cyan); margin-top: 6px; }
141
+
142
+ /* INSIGHTS */
143
+ .insight-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 24px; margin-bottom: 16px; }
144
+ .insight-card h3 { font-size: 15px; margin-bottom: 12px; }
145
+ .insight-card .insight-list { list-style: none; }
146
+ .insight-card .insight-list li { font-size: 13px; color: var(--text-muted); padding: 8px 0; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
147
+ .insight-card .insight-list li:last-child { border-bottom: none; }
148
+ .pro-badge { background: linear-gradient(135deg, var(--cyan-dim), rgba(167,139,250,0.12)); border: 1px solid rgba(34,211,238,0.3); border-radius: 10px; padding: 20px; text-align: center; margin: 20px 0; }
149
+ .pro-badge h3 { color: var(--cyan); margin-bottom: 8px; }
150
+ .pro-badge p { font-size: 13px; color: var(--text-muted); margin-bottom: 16px; }
151
+ .pro-badge a { color: var(--bg); background: var(--cyan); padding: 10px 24px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 14px; }
152
+ .pro-badge a:hover { opacity: 0.85; }
153
+
154
+ .empty { text-align: center; padding: 48px; color: var(--text-muted); font-size: 15px; }
155
+ .loading { text-align: center; padding: 24px; color: var(--text-muted); }
156
+ h2 { font-size: 20px; font-weight: 700; margin-bottom: 16px; letter-spacing: -0.02em; }
157
+
158
+ @media (max-width: 700px) {
159
+ .stats-grid { grid-template-columns: repeat(2, 1fr); }
160
+ .metrics-header { flex-direction: column; }
161
+ .metrics-summary-grid { grid-template-columns: repeat(2, 1fr); }
162
+ .metrics-chart { grid-template-columns: repeat(7, minmax(0, 1fr)); row-gap: 12px; }
163
+ .filter-row { flex-wrap: wrap; }
164
+ .rule-meta { flex-direction: column; gap: 8px; }
165
+ }
166
+
167
+ /* Feedback widget */
168
+ .feedback-widget { position: fixed; bottom: 20px; right: 20px; z-index: 1000; }
169
+ .feedback-toggle { background: var(--cyan); color: var(--bg); border: none; border-radius: 50px; padding: 12px 20px; font-size: 14px; font-weight: 600; cursor: pointer; box-shadow: 0 4px 12px rgba(34,211,238,0.3); transition: transform 0.15s; }
170
+ .feedback-toggle:hover { transform: scale(1.05); }
171
+ .feedback-panel { display: none; position: absolute; bottom: 56px; right: 0; width: 340px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 20px; box-shadow: 0 8px 24px rgba(0,0,0,0.4); }
172
+ .feedback-panel.open { display: block; }
173
+ .feedback-panel h3 { font-size: 15px; margin-bottom: 12px; }
174
+ .feedback-panel textarea { width: 100%; min-height: 80px; background: var(--bg); border: 1px solid var(--border); border-radius: 8px; color: var(--text); padding: 10px; font-size: 13px; font-family: inherit; resize: vertical; }
175
+ .feedback-panel textarea:focus { outline: none; border-color: var(--cyan); }
176
+ .feedback-panel select { width: 100%; background: var(--bg); border: 1px solid var(--border); border-radius: 8px; color: var(--text); padding: 8px 10px; font-size: 13px; margin: 8px 0; }
177
+ .feedback-submit { background: var(--cyan); color: var(--bg); border: none; border-radius: 8px; padding: 10px 20px; font-size: 13px; font-weight: 600; cursor: pointer; width: 100%; margin-top: 8px; }
178
+ .feedback-submit:hover { opacity: 0.85; }
179
+ .feedback-submit:disabled { opacity: 0.5; cursor: not-allowed; }
180
+ .feedback-success { color: var(--green); font-size: 13px; text-align: center; padding: 12px 0; }
181
+
182
+ </style>
183
+ </head>
184
+ <body>
185
+
186
+ <nav>
187
+ <div class="container">
188
+ <a href="/dashboard" class="nav-logo">👍👎 ThumbGate</a>
189
+ <div class="nav-links">
190
+ <a href="/dashboard">Dashboard</a>
191
+ <a href="/lessons" class="active">Lessons</a>
192
+ <a href="/">Landing Page</a>
193
+ <a href="https://github.com/IgorGanapolsky/ThumbGate" target="_blank" rel="noopener">GitHub</a>
194
+ </div>
195
+ </div>
196
+ </nav>
197
+
198
+ <div class="container">
199
+ <div style="margin:32px 0 24px;padding:24px;background:linear-gradient(135deg,rgba(167,139,250,0.08),rgba(34,211,238,0.05));border:1px solid rgba(167,139,250,0.2);border-radius:12px;">
200
+ <h1 style="font-size:22px;font-weight:700;margin-bottom:8px;letter-spacing:-0.02em;">📚 Lessons Learned</h1>
201
+ <p style="font-size:14px;color:var(--text-muted);line-height:1.6;max-width:700px;">See what ThumbGate learned from your feedback, which failure patterns keep repeating, and how many actions the gate layer actually blocked. <span style="color:var(--purple);font-weight:600;">This view separates repeated failures from recorded gate blocks so the proof stays honest.</span></p>
202
+ <div style="display:flex;gap:16px;margin-top:12px;font-size:12px;color:var(--text-muted);">
203
+ <span>📋 <strong style="color:var(--text);">Active Rules</strong> — what was learned</span>
204
+ <span>📊 <strong style="color:var(--text);">Timeline</strong> — when it was learned</span>
205
+ <span>💡 <strong style="color:var(--text);">Insights</strong> — what to do next</span>
206
+ </div>
207
+ </div>
208
+ <div class="stats-grid">
209
+ <div class="stat-card" onclick="switchTab('rules'); filterSeverity('all')">
210
+ <div class="stat-label">Active Rules</div>
211
+ <div class="stat-value cyan" id="statRules">0</div>
212
+ <div class="stat-sub">Auto-generated from feedback</div>
213
+ </div>
214
+ <div class="stat-card" onclick="switchTab('rules'); filterSeverity('critical')">
215
+ <div class="stat-label">Critical</div>
216
+ <div class="stat-value red" id="statCritical">0</div>
217
+ <div class="stat-sub">Highest severity rules</div>
218
+ </div>
219
+ <div class="stat-card" onclick="switchTab('timeline')">
220
+ <div class="stat-label">Actions Blocked</div>
221
+ <div class="stat-value green" id="statBlocked">0</div>
222
+ <div class="stat-sub">Recorded gate denies, not inferred repeats</div>
223
+ </div>
224
+ <div class="stat-card" onclick="switchTab('insights')">
225
+ <div class="stat-label">Approval Trend</div>
226
+ <div class="stat-value purple" id="statTrend">--</div>
227
+ <div class="stat-sub" id="statTrendSub">7-day rolling</div>
228
+ </div>
229
+ </div>
230
+
231
+ <div id="lessonsMode" style="margin:-8px 0 20px;padding:12px 16px;border:1px solid var(--border);border-radius:10px;background:var(--bg-card);font-size:13px;color:var(--text-muted);">
232
+ Loading lessons view...
233
+ </div>
234
+
235
+ <div class="metrics-panel" id="metricsPanel">
236
+ <div class="metrics-header">
237
+ <div>
238
+ <h2>Improvement Over Time</h2>
239
+ <p id="metricsSummary">Loading defensible live metrics...</p>
240
+ </div>
241
+ <button class="filter-btn" id="metricsClearBtn" type="button" style="display:none;" onclick="clearTimelineDayFilter()">Clear day filter</button>
242
+ </div>
243
+ <div class="metrics-summary-grid" id="metricsSummaryGrid">
244
+ <div class="metric-tile">
245
+ <div class="metric-kicker">Fast path rate</div>
246
+ <div class="metric-number purple">--</div>
247
+ <div class="metric-note">Waiting for live decisions</div>
248
+ </div>
249
+ <div class="metric-tile">
250
+ <div class="metric-kicker">Override rate</div>
251
+ <div class="metric-number red">--</div>
252
+ <div class="metric-note">Waiting for live decisions</div>
253
+ </div>
254
+ <div class="metric-tile">
255
+ <div class="metric-kicker">Rollback rate</div>
256
+ <div class="metric-number cyan">--</div>
257
+ <div class="metric-note">Waiting for live decisions</div>
258
+ </div>
259
+ <div class="metric-tile">
260
+ <div class="metric-kicker">Median latency</div>
261
+ <div class="metric-number green">--</div>
262
+ <div class="metric-note">Time from recommendation to recorded outcome</div>
263
+ </div>
264
+ </div>
265
+ <div class="metrics-chart-wrap">
266
+ <div class="metrics-chart-title">
267
+ <h3>Recent Feedback + Gate Activity</h3>
268
+ <p>Click any day to inspect the recorded feedback behind the trend and compare it with gate interceptions.</p>
269
+ </div>
270
+ <div class="metrics-legend">
271
+ <span><i class="metrics-legend-dot up"></i> Positive feedback</span>
272
+ <span><i class="metrics-legend-dot down"></i> Negative feedback</span>
273
+ <span><i class="metrics-legend-dot deny"></i> Gate deny</span>
274
+ <span><i class="metrics-legend-dot warn"></i> Gate warn</span>
275
+ </div>
276
+ <div class="metrics-chart" id="metricsChart">
277
+ <div class="loading" style="grid-column:1 / -1;">Loading chart...</div>
278
+ </div>
279
+ <div class="metrics-chart-note" id="metricsChartNote">The chart uses recorded feedback events, not inferred rule strength.</div>
280
+ </div>
281
+ </div>
282
+
283
+ <div class="tabs">
284
+ <div class="tab active" onclick="switchTab('rules')">📋 Active Rules</div>
285
+ <div class="tab" onclick="switchTab('timeline')">📊 Feedback Timeline</div>
286
+ <div class="tab" onclick="switchTab('insights')">💡 Insights <span style="font-size:10px;color:var(--purple);font-weight:700;">PRO</span></div>
287
+ </div>
288
+
289
+ <!-- TAB 1: ACTIVE RULES -->
290
+ <div class="tab-content active" id="tab-rules">
291
+ <div class="search-bar">
292
+ <input type="text" class="search-input" id="ruleSearch" placeholder="Search rules... (try: git, push, test, env)" onkeydown="if(event.key==='Enter')searchRules()">
293
+ <button class="btn" onclick="searchRules()">Search</button>
294
+ </div>
295
+ <div class="filter-row">
296
+ <button class="filter-btn active" onclick="filterSeverity('all', this)">All</button>
297
+ <button class="filter-btn" onclick="filterSeverity('critical', this)">Critical</button>
298
+ <button class="filter-btn" onclick="filterSeverity('warning', this)">Warning</button>
299
+ <button class="filter-btn" onclick="filterSeverity('info', this)">Info</button>
300
+ </div>
301
+ <div class="rules-list" id="rulesList">
302
+ <div class="loading">Loading rules...</div>
303
+ </div>
304
+ </div>
305
+
306
+ <!-- TAB 2: FEEDBACK TIMELINE -->
307
+ <div class="tab-content" id="tab-timeline">
308
+ <div class="filter-row">
309
+ <button class="filter-btn active" onclick="filterTimeline('all', this)">All</button>
310
+ <button class="filter-btn" onclick="filterTimeline('up', this)">👍 Positive</button>
311
+ <button class="filter-btn" onclick="filterTimeline('down', this)">👎 Negative</button>
312
+ </div>
313
+ <div class="timeline" id="timelineList">
314
+ <div class="loading">Loading timeline...</div>
315
+ </div>
316
+ </div>
317
+
318
+ <!-- TAB 3: INSIGHTS (PRO) -->
319
+ <div class="tab-content" id="tab-insights">
320
+ <div class="insight-card">
321
+ <h3>🔥 Top Recurring Failure Patterns</h3>
322
+ <ul class="insight-list" id="insightMistakes">
323
+ <li><span>Loading...</span></li>
324
+ </ul>
325
+ </div>
326
+ <div class="insight-card">
327
+ <h3>🛡️ Most Reinforced Rules</h3>
328
+ <ul class="insight-list" id="insightEffective">
329
+ <li><span>Loading...</span></li>
330
+ </ul>
331
+ </div>
332
+ <div class="insight-card">
333
+ <h3>🗑️ Stale Rules (no triggers in 30 days)</h3>
334
+ <ul class="insight-list" id="insightStale">
335
+ <li><span>Loading...</span></li>
336
+ </ul>
337
+ </div>
338
+ <div class="pro-badge" id="proBadge" style="display:none;">
339
+ <h3>Unlock Full Insights</h3>
340
+ <p>DPO training data export, effectiveness charts, pattern clustering, weekly auto-digests, and a learned intervention policy trained from feedback, audits, and diagnostics.</p>
341
+ <a href="/#pricing">Get Pro — $19/mo</a>
342
+ </div>
343
+ </div>
344
+ </div>
345
+
346
+ <script>
347
+ var allRules = [];
348
+ var allTimeline = [];
349
+ var isDemo = true;
350
+ var API_KEY = '';
351
+ var dashboardSnapshot = null;
352
+ var activeTimelineSignal = 'all';
353
+ var activeTimelineDay = null;
354
+ var currentImprovementSeries = [];
355
+ const BOOTSTRAP_API_KEY = __LESSONS_BOOTSTRAP_KEY__;
356
+ const LOCAL_PRO_BOOTSTRAP = __LESSONS_BOOTSTRAP_ENABLED__;
357
+
358
+ function escHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
359
+ function escAttr(s) { return escHtml(String(s || '')).replace(/"/g, '&quot;'); }
360
+
361
+ function getHeaders() {
362
+ return { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' };
363
+ }
364
+
365
+ function hasBootstrapKey() {
366
+ return LOCAL_PRO_BOOTSTRAP && Boolean(BOOTSTRAP_API_KEY);
367
+ }
368
+
369
+ function setMode(message, demoMode) {
370
+ isDemo = demoMode;
371
+ var el = document.getElementById('lessonsMode');
372
+ if (!el) return;
373
+ el.textContent = message;
374
+ el.style.borderColor = demoMode ? 'var(--border)' : 'rgba(74,222,128,0.25)';
375
+ el.style.color = demoMode ? 'var(--text-muted)' : 'var(--green)';
376
+ }
377
+
378
+ function formatDate(value) {
379
+ if (!value) return '';
380
+ var ts = new Date(value);
381
+ if (Number.isNaN(ts.getTime())) return String(value);
382
+ return ts.toLocaleString(undefined, { year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: '2-digit', second: '2-digit', timeZoneName: 'short' });
383
+ }
384
+
385
+ function formatShortDay(dayKey) {
386
+ if (!dayKey) return '';
387
+ var ts = new Date(dayKey + 'T00:00:00');
388
+ if (Number.isNaN(ts.getTime())) return dayKey;
389
+ return ts.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
390
+ }
391
+
392
+ function normalizeSignal(value) {
393
+ var signal = String(value || '').toLowerCase();
394
+ if (signal === 'up' || signal === 'positive' || signal === 'thumbs_up') return 'up';
395
+ return 'down';
396
+ }
397
+
398
+ function toDayKey(value) {
399
+ if (!value) return '';
400
+ var ts = new Date(value);
401
+ if (Number.isNaN(ts.getTime())) return '';
402
+ var year = ts.getFullYear();
403
+ var month = String(ts.getMonth() + 1).padStart(2, '0');
404
+ var day = String(ts.getDate()).padStart(2, '0');
405
+ return year + '-' + month + '-' + day;
406
+ }
407
+
408
+ function highlightCard(index) {
409
+ document.querySelectorAll('.stat-card').forEach(function(c, i) {
410
+ c.classList.toggle('selected', i === index);
411
+ });
412
+ }
413
+ function switchTab(name) {
414
+ // Highlight the correct tab header (only inside .tabs, not stat cards)
415
+ document.querySelectorAll('.tabs .tab').forEach(function(t) { t.classList.remove('active'); });
416
+ document.querySelectorAll('.tab-content').forEach(function(c) { c.classList.remove('active'); });
417
+ var tabHeaders = document.querySelectorAll('.tabs .tab');
418
+ var tabMap = { rules: 0, timeline: 1, insights: 2 };
419
+ if (tabHeaders[tabMap[name]]) tabHeaders[tabMap[name]].classList.add('active');
420
+ var content = document.getElementById('tab-' + name);
421
+ if (content) content.classList.add('active');
422
+ // Highlight the corresponding stat card
423
+ var cardMap = { rules: 0, timeline: 2, insights: 3 };
424
+ highlightCard(cardMap[name] !== undefined ? cardMap[name] : -1);
425
+ if (typeof plausible === 'function') plausible('lessons_tab', { props: { tab: name } });
426
+ }
427
+
428
+ function filterSeverity(level, el) {
429
+ // Update filter button active state
430
+ var filterBtns = document.querySelectorAll('#tab-rules .filter-btn');
431
+ filterBtns.forEach(function(b) { b.classList.remove('active'); });
432
+ if (el) {
433
+ el.classList.add('active');
434
+ } else {
435
+ // Find the matching button and activate it
436
+ filterBtns.forEach(function(b) {
437
+ if (b.textContent.trim().toLowerCase() === level || (level === 'all' && b.textContent.trim() === 'All')) {
438
+ b.classList.add('active');
439
+ }
440
+ });
441
+ }
442
+ // Highlight Critical card (index 1) when filtering critical, else Active Rules card (index 0)
443
+ if (level === 'critical') { highlightCard(1); } else { highlightCard(0); }
444
+ var filtered = level === 'all' ? allRules : allRules.filter(function(r) { return r.severity === level; });
445
+ renderRules(filtered);
446
+ }
447
+
448
+ function filterTimeline(signal, el) {
449
+ activeTimelineSignal = signal;
450
+ if (el) {
451
+ document.querySelectorAll('#tab-timeline .filter-btn').forEach(function(b) { b.classList.remove('active'); });
452
+ el.classList.add('active');
453
+ }
454
+ renderTimeline(getFilteredTimelineItems());
455
+ updateMetricsSelection();
456
+ }
457
+
458
+ function getFilteredTimelineItems() {
459
+ return allTimeline.filter(function(item) {
460
+ if (activeTimelineSignal !== 'all' && item.signal !== activeTimelineSignal) return false;
461
+ if (activeTimelineDay && item.dayKey !== activeTimelineDay) return false;
462
+ return true;
463
+ });
464
+ }
465
+
466
+ function focusTimelineDay(dayKey) {
467
+ activeTimelineDay = dayKey;
468
+ switchTab('timeline');
469
+ renderTimeline(getFilteredTimelineItems());
470
+ updateMetricsSelection();
471
+ }
472
+
473
+ function clearTimelineDayFilter() {
474
+ activeTimelineDay = null;
475
+ renderTimeline(getFilteredTimelineItems());
476
+ updateMetricsSelection();
477
+ }
478
+
479
+ function updateMetricsSelection() {
480
+ document.querySelectorAll('.metrics-bar').forEach(function(bar) {
481
+ bar.classList.toggle('selected', activeTimelineDay && bar.getAttribute('data-day-key') === activeTimelineDay);
482
+ });
483
+ var clearBtn = document.getElementById('metricsClearBtn');
484
+ if (clearBtn) clearBtn.style.display = activeTimelineDay ? 'inline-flex' : 'none';
485
+ var note = document.getElementById('metricsChartNote');
486
+ if (!note) return;
487
+ if (activeTimelineDay) {
488
+ var selected = currentImprovementSeries.find(function(item) { return item.dayKey === activeTimelineDay; }) || null;
489
+ if (selected) {
490
+ note.textContent = 'Showing timeline events for ' + formatShortDay(activeTimelineDay) + '. Feedback: ' + selected.total + ' (' + selected.up + ' positive, ' + selected.down + ' negative). Gate audit: ' + selected.intercepted + ' intercepted (' + selected.deny + ' denies, ' + selected.warn + ' warns).';
491
+ } else {
492
+ note.textContent = 'Showing timeline events for ' + formatShortDay(activeTimelineDay) + '. Clear the day filter to return to the full timeline.';
493
+ }
494
+ } else {
495
+ note.textContent = 'The chart combines recorded feedback events with daily gate-audit activity. Decision-loop metrics above come from recorded evaluations and outcomes.';
496
+ }
497
+ }
498
+
499
+ function searchRules() {
500
+ var q = document.getElementById('ruleSearch').value.toLowerCase().trim();
501
+ if (!q) { renderRules(allRules); return; }
502
+ var filtered = allRules.filter(function(r) {
503
+ return (r.title || '').toLowerCase().includes(q) ||
504
+ (r.context || '').toLowerCase().includes(q) ||
505
+ (r.tags || []).some(function(t) { return t.toLowerCase().includes(q); });
506
+ });
507
+ renderRules(filtered);
508
+ }
509
+
510
+ function getSeverity(rule) {
511
+ if (rule.occurrences >= 10 || (rule.tags || []).includes('CRITICAL')) return 'critical';
512
+ if (rule.occurrences >= 3) return 'warning';
513
+ return 'info';
514
+ }
515
+
516
+ function renderRules(rules) {
517
+ var el = document.getElementById('rulesList');
518
+ if (!rules.length) { el.innerHTML = '<div class="empty">No rules match your filter</div>'; return; }
519
+ el.innerHTML = rules.map(function(r) {
520
+ var sev = r.severity || getSeverity(r);
521
+ return '<div class="rule-card" data-rule-id="' + escHtml(r.id || '') + '">' +
522
+ '<div class="rule-header">' +
523
+ '<div class="rule-title"><a href="/lessons/' + encodeURIComponent(r.id || '') + '" style="color:inherit;text-decoration:none" onmouseover="this.style.color=\'var(--cyan)\'" onmouseout="this.style.color=\'inherit\'">' + escHtml(r.title || r.id || 'Rule') + '</a></div>' +
524
+ '<span class="rule-severity ' + sev + '">' + sev + '</span>' +
525
+ '</div>' +
526
+ (r.context ? '<div class="rule-context">' + escHtml(r.context) + '</div>' : '') +
527
+ '<div class="rule-meta">' +
528
+ '<div class="rule-meta-item">Seen in feedback <span class="value">' + (r.occurrences || 0) + '</span> times</div>' +
529
+ (r.lastTriggered ? '<div class="rule-meta-item">Last: <span class="value">' + escHtml(r.lastTriggered) + '</span></div>' : '') +
530
+ '</div>' +
531
+ (r.tags && r.tags.length ? '<div class="rule-tags">' + r.tags.map(function(t) { return '<span class="tag">' + escHtml(t) + '</span>'; }).join('') + '</div>' : '') +
532
+ '</div>';
533
+ }).join('');
534
+ }
535
+
536
+ function renderTimeline(items) {
537
+ var el = document.getElementById('timelineList');
538
+ if (!items.length) { el.innerHTML = '<div class="empty">No feedback events</div>'; return; }
539
+ el.innerHTML = items.map(function(item) {
540
+ var sig = item.signal || 'down';
541
+ var emoji = sig === 'up' ? '👍' : '👎';
542
+ var fbId = item.feedbackId || item.id || '';
543
+ return '<div class="timeline-item" data-feedback-id="' + escHtml(fbId) + '">' +
544
+ '<div class="timeline-dot ' + sig + '"></div>' +
545
+ '<div class="timeline-card">' +
546
+ '<div class="timeline-header">' +
547
+ '<span class="timeline-signal ' + sig + '">' + emoji + ' ' + (sig === 'up' ? 'Positive' : 'Negative') + '</span>' +
548
+ '<span class="timeline-date">' + escHtml(item.date || '') + '</span>' +
549
+ '</div>' +
550
+ '<div class="timeline-context"><a href="/lessons/' + encodeURIComponent(item.feedbackId || '') + '" style="color:inherit;text-decoration:none" onmouseover="this.style.color=\'var(--cyan)\'" onmouseout="this.style.color=\'inherit\'">' + escHtml(item.context || item.title || '') + '</a></div>' +
551
+ (item.learned ? '<div class="timeline-learned">→ Rule created: ' + escHtml(item.learned) + '</div>' : '') +
552
+ '</div>' +
553
+ '</div>';
554
+ }).join('');
555
+ }
556
+
557
+ function renderInsights(data) {
558
+ var mistakes = document.getElementById('insightMistakes');
559
+ var effective = document.getElementById('insightEffective');
560
+ var stale = document.getElementById('insightStale');
561
+
562
+ // Dedup by title — keep the one with highest occurrences
563
+ var seen = {};
564
+ var dedupedRules = allRules.filter(function(r) {
565
+ var key = (r.title || '').trim().toLowerCase();
566
+ if (!key) return false;
567
+ if (seen[key] && seen[key].occurrences >= (r.occurrences || 0)) return false;
568
+ seen[key] = r;
569
+ return true;
570
+ });
571
+
572
+ var topMistakes = dedupedRules.filter(function(r) { return r.severity === 'critical'; })
573
+ .sort(function(a, b) { return (b.occurrences || 0) - (a.occurrences || 0); }).slice(0, 5);
574
+ var topEffective = dedupedRules.slice().sort(function(a, b) { return (b.occurrences || 0) - (a.occurrences || 0); })
575
+ .filter(function(r) { return (r.occurrences || 0) > 1; }).slice(0, 5);
576
+ var staleRules = dedupedRules.filter(function(r) { return !r.lastTriggered || r.lastTriggered === 'never'; });
577
+
578
+ function insightRow(r, badge) {
579
+ var title = r.title.length > 80 ? r.title.slice(0, 77) + '...' : r.title;
580
+ return '<li style="cursor:pointer" onclick="switchTab(\'rules\'); searchAndHighlight(\'' + escHtml(r.id) + '\')">'
581
+ + '<span title="' + escHtml(r.title) + '">' + escHtml(title) + '</span>' + badge + '</li>';
582
+ }
583
+
584
+ mistakes.innerHTML = topMistakes.length ? topMistakes.map(function(r) {
585
+ return insightRow(r, '<span style="color:var(--red);font-weight:600">' + (r.occurrences || 0) + 'x</span>');
586
+ }).join('') : '<li><span style="color:var(--green)">No critical repeated failures this week</span></li>';
587
+
588
+ effective.innerHTML = topEffective.length ? topEffective.map(function(r) {
589
+ return insightRow(r, '<span class="rule-effectiveness">Seen ' + (r.occurrences || 0) + 'x</span>');
590
+ }).join('') : '<li><span>No reinforced rules yet</span></li>';
591
+
592
+ stale.innerHTML = staleRules.length ? staleRules.map(function(r) {
593
+ return insightRow(r, '<span style="color:var(--text-muted)">Archive?</span>');
594
+ }).join('') : '<li><span style="color:var(--green)">All rules are active</span></li>';
595
+ }
596
+
597
+ function buildFeedbackSeries(items, dayCount) {
598
+ var series = [];
599
+ var today = new Date();
600
+ today.setHours(0, 0, 0, 0);
601
+ var counts = {};
602
+ items.forEach(function(item) {
603
+ if (!item.dayKey) return;
604
+ if (!counts[item.dayKey]) counts[item.dayKey] = { up: 0, down: 0 };
605
+ counts[item.dayKey][item.signal] = (counts[item.dayKey][item.signal] || 0) + 1;
606
+ });
607
+ for (var idx = dayCount - 1; idx >= 0; idx--) {
608
+ var day = new Date(today);
609
+ day.setDate(today.getDate() - idx);
610
+ var dayKey = toDayKey(day.toISOString());
611
+ var entry = counts[dayKey] || { up: 0, down: 0 };
612
+ series.push({
613
+ dayKey: dayKey,
614
+ label: formatShortDay(dayKey),
615
+ up: entry.up || 0,
616
+ down: entry.down || 0,
617
+ total: (entry.up || 0) + (entry.down || 0)
618
+ });
619
+ }
620
+ return series;
621
+ }
622
+
623
+ function renderImprovementMetrics(stats, data) {
624
+ var summary = document.getElementById('metricsSummary');
625
+ var grid = document.getElementById('metricsSummaryGrid');
626
+ var chart = document.getElementById('metricsChart');
627
+ if (!summary || !grid || !chart) return;
628
+
629
+ if (isDemo) {
630
+ summary.textContent = 'Live improvement trends appear here once local Pro is connected. The decision tiles are based on recorded evaluations plus override and rollback outcomes, while the chart below stays anchored to feedback and gate audits.';
631
+ grid.innerHTML = [
632
+ { label: 'Fast path rate', value: '62%', tone: 'purple', note: 'Sample auto-execute share' },
633
+ { label: 'Override rate', value: '14%', tone: 'red', note: 'Sample operator overrides' },
634
+ { label: 'Rollback rate', value: '4%', tone: 'cyan', note: 'Sample reversed decisions' },
635
+ { label: 'Median latency', value: '6m', tone: 'green', note: 'Sample decision completion speed' }
636
+ ].map(function(tile) {
637
+ return '<div class="metric-tile"><div class="metric-kicker">' + escHtml(tile.label) + '</div><div class="metric-number ' + escHtml(tile.tone) + '">' + escHtml(tile.value) + '</div><div class="metric-note">' + escHtml(tile.note) + '</div></div>';
638
+ }).join('');
639
+ currentImprovementSeries = [];
640
+ chart.innerHTML = '<div class="empty" style="grid-column:1 / -1;">Upgrade to Pro to see the clickable live feedback and gate-audit timeline.</div>';
641
+ updateMetricsSelection();
642
+ return;
643
+ }
644
+
645
+ var gateStats = (data && data.gateStats) || {};
646
+ var gateAudit = (data && data.gateAudit) || {};
647
+ var liveMetrics = (data && data.liveMetrics) || {};
648
+ var decisionLoop = (data && data.decisions) || {};
649
+ var harness = (data && data.harness) || {};
650
+ var approvalRate = Math.round(((stats && (stats.recentRate || stats.approvalRate)) || 0) * 100);
651
+ var errorTrend = liveMetrics.errorTrend || {};
652
+ var fastPathRate = Math.round(((decisionLoop.fastPathRate || 0) * 100));
653
+ var overrideRate = Math.round(((decisionLoop.overrideRate || 0) * 100));
654
+ var rollbackRate = Math.round(((decisionLoop.rollbackRate || 0) * 100));
655
+ var medianLatencyMs = Number(decisionLoop.medianLatencyMs || 0);
656
+ var medianLatencyText = medianLatencyMs >= 3600000
657
+ ? (medianLatencyMs / 3600000).toFixed(1) + 'h'
658
+ : medianLatencyMs >= 60000
659
+ ? Math.round(medianLatencyMs / 60000) + 'm'
660
+ : Math.round(medianLatencyMs / 1000) + 's';
661
+ var thisWeekNeg = errorTrend.thisWeek || 0;
662
+ var lastWeekNeg = errorTrend.lastWeek || 0;
663
+ var blocked = gateStats.blocked || 0;
664
+ var resolvedCount = decisionLoop.resolvedCount || 0;
665
+
666
+ summary.textContent = 'ThumbGate is auto-routing ' + fastPathRate + '% of tracked decisions while holding override rate at ' + overrideRate + '% and rollback rate at ' + rollbackRate + '% across '
667
+ + resolvedCount + ' resolved decision' + (resolvedCount === 1 ? '' : 's') + '. Feedback approval is ' + approvalRate + '%, and gates still recorded ' + blocked + ' deny decision' + (blocked === 1 ? '' : 's') + '.';
668
+
669
+ grid.innerHTML = [
670
+ { label: 'Fast path rate', value: fastPathRate + '%', tone: 'purple', note: 'Recorded evaluations that stayed auto-executable' },
671
+ { label: 'Override rate', value: overrideRate + '%', tone: 'red', note: 'Resolved decisions later changed by a human' },
672
+ { label: 'Rollback rate', value: rollbackRate + '%', tone: 'cyan', note: 'Resolved decisions later reversed' },
673
+ { label: 'Median latency', value: medianLatencyText, tone: 'green', note: 'Time from recommendation to recorded outcome' }
674
+ ].map(function(tile) {
675
+ return '<div class="metric-tile"><div class="metric-kicker">' + escHtml(tile.label) + '</div><div class="metric-number ' + escHtml(tile.tone) + '">' + escHtml(tile.value) + '</div><div class="metric-note">' + escHtml(tile.note) + '</div></div>';
676
+ }).join('');
677
+
678
+ var gateDays = {};
679
+ ((gateAudit.days) || []).forEach(function(day) {
680
+ gateDays[day.dayKey] = day;
681
+ });
682
+ var series = buildFeedbackSeries(allTimeline, 14).map(function(item) {
683
+ var gateDay = gateDays[item.dayKey] || { deny: 0, warn: 0, intercepted: 0 };
684
+ return {
685
+ dayKey: item.dayKey,
686
+ label: item.label,
687
+ up: item.up,
688
+ down: item.down,
689
+ total: item.total,
690
+ deny: gateDay.deny || 0,
691
+ warn: gateDay.warn || 0,
692
+ intercepted: gateDay.intercepted || 0
693
+ };
694
+ });
695
+ currentImprovementSeries = series;
696
+ var maxFeedbackTotal = series.reduce(function(max, item) { return Math.max(max, item.total); }, 0);
697
+ var maxGateTotal = series.reduce(function(max, item) { return Math.max(max, item.intercepted); }, 0);
698
+ if (maxFeedbackTotal === 0 && maxGateTotal === 0) {
699
+ currentImprovementSeries = [];
700
+ chart.innerHTML = '<div class="empty" style="grid-column:1 / -1;">No recent feedback events to chart yet.</div>';
701
+ updateMetricsSelection();
702
+ return;
703
+ }
704
+
705
+ chart.innerHTML = series.map(function(item) {
706
+ if (!item.total && !item.intercepted) {
707
+ return '<button class="metrics-bar" type="button" data-day-key="' + escAttr(item.dayKey) + '" onclick="focusTimelineDay(\'' + escAttr(item.dayKey) + '\')">' +
708
+ '<div class="metrics-bar-body"><div class="metrics-bar-stack"><div class="metrics-bar-empty"></div></div><div class="metrics-bar-stack gate"><div class="metrics-bar-empty"></div></div></div>' +
709
+ '<div class="metrics-bar-total">F0</div>' +
710
+ '<div class="metrics-bar-subtotal">G0</div>' +
711
+ '<div class="metrics-bar-label">' + escHtml(item.label) + '</div>' +
712
+ '</button>';
713
+ }
714
+ var feedbackHeight = item.total > 0 ? Math.max(14, Math.round((item.total / Math.max(maxFeedbackTotal, 1)) * 120)) : 0;
715
+ var upHeight = item.up > 0 ? Math.max(8, Math.round((item.up / Math.max(item.total, 1)) * feedbackHeight)) : 0;
716
+ var downHeight = item.down > 0 ? Math.max(8, feedbackHeight - upHeight) : 0;
717
+ if (item.up > 0 && item.down > 0 && upHeight + downHeight > 120) downHeight = Math.max(8, 120 - upHeight);
718
+ var gateHeight = item.intercepted > 0 ? Math.max(12, Math.round((item.intercepted / Math.max(maxGateTotal, 1)) * 84)) : 0;
719
+ var denyHeight = item.deny > 0 ? Math.max(8, Math.round((item.deny / Math.max(item.intercepted, 1)) * gateHeight)) : 0;
720
+ var warnHeight = item.warn > 0 ? Math.max(8, gateHeight - denyHeight) : 0;
721
+ if (item.deny > 0 && item.warn > 0 && denyHeight + warnHeight > 84) warnHeight = Math.max(8, 84 - denyHeight);
722
+ return '<button class="metrics-bar" type="button" data-day-key="' + escAttr(item.dayKey) + '" onclick="focusTimelineDay(\'' + escAttr(item.dayKey) + '\')">' +
723
+ '<div class="metrics-bar-body">' +
724
+ '<div class="metrics-bar-stack">' +
725
+ (item.up ? '<div class="metrics-segment up" style="height:' + upHeight + 'px" title="' + item.up + ' positive feedback"></div>' : '') +
726
+ (item.down ? '<div class="metrics-segment down" style="height:' + downHeight + 'px" title="' + item.down + ' negative feedback"></div>' : '') +
727
+ (!item.total ? '<div class="metrics-bar-empty"></div>' : '') +
728
+ '</div>' +
729
+ '<div class="metrics-bar-stack gate">' +
730
+ (item.deny ? '<div class="metrics-segment deny" style="height:' + denyHeight + 'px" title="' + item.deny + ' gate denies"></div>' : '') +
731
+ (item.warn ? '<div class="metrics-segment warn" style="height:' + warnHeight + 'px" title="' + item.warn + ' gate warns"></div>' : '') +
732
+ (!item.intercepted ? '<div class="metrics-bar-empty"></div>' : '') +
733
+ '</div>' +
734
+ '</div>' +
735
+ '<div class="metrics-bar-total">F' + item.total + '</div>' +
736
+ '<div class="metrics-bar-subtotal">G' + item.intercepted + '</div>' +
737
+ '<div class="metrics-bar-label">' + escHtml(item.label) + '</div>' +
738
+ '</button>';
739
+ }).join('');
740
+ updateMetricsSelection();
741
+ }
742
+
743
+ function searchAndHighlight(ruleId) {
744
+ var el = document.querySelector('[data-rule-id="' + ruleId + '"]');
745
+ if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); el.style.borderColor = 'var(--cyan)'; setTimeout(function() { el.style.borderColor = ''; }, 2000); }
746
+ }
747
+
748
+ function deriveSeverity(lesson) {
749
+ var severity = String(lesson.importance || '').toLowerCase();
750
+ if (severity === 'critical' || severity === 'high') return 'critical';
751
+ if (severity === 'medium') return 'warning';
752
+ return 'info';
753
+ }
754
+
755
+ function mapLiveRules(payload) {
756
+ return (payload.results || []).map(function(result) {
757
+ var linkedGate = (((result.systemResponse || {}).linkedAutoGates) || [])[0] || null;
758
+ var sourceFeedback = ((result.systemResponse || {}).sourceFeedback) || null;
759
+ return {
760
+ id: result.id,
761
+ title: result.title || ((result.lesson || {}).summary) || 'Lesson',
762
+ context: ((result.lesson || {}).howToAvoid) || ((result.lesson || {}).summary) || (sourceFeedback && sourceFeedback.context) || '',
763
+ occurrences: linkedGate && linkedGate.occurrences ? linkedGate.occurrences : 1,
764
+ severity: deriveSeverity(result),
765
+ tags: Array.isArray(result.tags) ? result.tags : [],
766
+ lastTriggered: sourceFeedback && sourceFeedback.timestamp ? formatDate(sourceFeedback.timestamp) : formatDate(result.timestamp),
767
+ sourceFeedbackId: sourceFeedback && sourceFeedback.id ? sourceFeedback.id : result.sourceFeedbackId
768
+ };
769
+ });
770
+ }
771
+
772
+ function mapTimelineItems(payload, lessonMap) {
773
+ return (payload.results || []).map(function(entry) {
774
+ var linkedLesson = lessonMap.get(entry.id) || null;
775
+ var timestamp = entry.timestamp || null;
776
+ return {
777
+ signal: normalizeSignal(entry.signal || entry.feedback),
778
+ context: entry.context || entry.title || '',
779
+ date: formatDate(entry.timestamp),
780
+ timestamp: timestamp,
781
+ dayKey: toDayKey(timestamp),
782
+ learned: linkedLesson ? linkedLesson.title : '',
783
+ feedbackId: entry.id || ''
784
+ };
785
+ });
786
+ }
787
+
788
+ // Demo data
789
+ var demoRules = [
790
+ { id: 'execution-gap', title: 'Push before claiming done', context: 'After tests pass, immediately git add + commit + push. Never announce completion without pushing.', occurrences: 36, prevented: 22, severity: 'critical', tags: ['execution-gap', 'git-workflow'], lastTriggered: '2 hours ago' },
791
+ { id: 'anti-lying', title: 'Never claim without evidence', context: 'Check with tools before claiming anything. Show evidence. Say "I don\'t know" instead of guessing.', occurrences: 24, prevented: 15, severity: 'critical', tags: ['anti-lying', 'trust'], lastTriggered: '1 hour ago' },
792
+ { id: 'speed', title: 'Switch strategy after first failure', context: 'Prefer text-based assertions over slow UI tools. No repeated retries.', occurrences: 21, prevented: 12, severity: 'critical', tags: ['speed', 'efficiency'], lastTriggered: '3 hours ago' },
793
+ { id: 'pr-review', title: 'Check PR state after every push', context: 'Run gh pr view after push. Code changes alone do NOT resolve conversations.', occurrences: 15, prevented: 8, severity: 'warning', tags: ['pr-review', 'verification'], lastTriggered: '5 hours ago' },
794
+ { id: 'git-workflow', title: 'Tests pass then add-commit-push', context: 'Never announce completion without pushing. Verify git diff HEAD origin is empty.', occurrences: 12, prevented: 7, severity: 'warning', tags: ['git-workflow', 'push'], lastTriggered: 'yesterday' },
795
+ { id: 'delegation', title: 'Execute, don\'t ask permission', context: 'On clear instructions, execute immediately. Never ask "want me to do X?"', occurrences: 11, prevented: 6, severity: 'warning', tags: ['delegation', 'autonomy'], lastTriggered: 'yesterday' },
796
+ { id: 'config-before-code', title: 'curl first, never edit .env', context: 'On API failures: curl endpoint, check .env, check Postman, check Slack. Never modify code for auth issues.', occurrences: 4, prevented: 3, severity: 'info', tags: ['config', 'api', 'auth'], lastTriggered: '3 days ago' },
797
+ { id: 'jest-hoisting', title: 'No const/let before jest.mock()', context: 'jest.mock() is hoisted. Use captured-props pattern instead.', occurrences: 2, prevented: 1, severity: 'info', tags: ['jest', 'testing'], lastTriggered: '1 week ago' },
798
+ ];
799
+
800
+ var demoTimeline = [
801
+ { signal: 'down', context: 'Claimed fix done without pushing code', date: '2 hours ago', learned: 'Push before claiming done' },
802
+ { signal: 'down', context: 'Said endpoint not deployed without checking', date: '3 hours ago', learned: 'Never claim without evidence' },
803
+ { signal: 'up', context: 'Clean 4-tier pricing implementation with tests', date: '5 hours ago' },
804
+ { signal: 'down', context: 'Made false claims about what was broken', date: '6 hours ago', learned: 'Never claim without evidence' },
805
+ { signal: 'up', context: 'Statusline reorder done correctly first try', date: '8 hours ago' },
806
+ { signal: 'down', context: 'Delegated to subagent without verifying output', date: 'yesterday', learned: 'Execute, don\'t ask permission' },
807
+ { signal: 'up', context: 'Evidence-based PR management with iterative fixes', date: 'yesterday' },
808
+ { signal: 'down', context: 'Spent 45min on code when curl would have diagnosed in 30s', date: '3 days ago', learned: 'curl first, never edit .env' },
809
+ ];
810
+
811
+ function renderUpgradeWall(containerId) {
812
+ var el = document.getElementById(containerId);
813
+ if (!el) return;
814
+ var wall = document.createElement('div');
815
+ wall.style.cssText = 'position:relative;margin-top:8px;';
816
+ wall.innerHTML = '<div style="position:absolute;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;z-index:2;">' +
817
+ '<div style="text-align:center;background:rgba(10,10,15,0.92);border:1px solid #333;border-radius:12px;padding:28px 36px;">' +
818
+ '<div style="font-size:20px;font-weight:700;color:#fff;margin-bottom:8px;">Unlock your full lessons</div>' +
819
+ '<div style="color:#aaa;margin-bottom:16px;">Pro shows your real prevention rules, timeline, and insights.</div>' +
820
+ '<a href="https://buy.stripe.com/7sYcN5bmIf5IcSd8qf3sI0a" target="_blank" rel="noopener" ' +
821
+ 'style="display:inline-block;background:#b85c2d;color:#fff;padding:10px 24px;border-radius:8px;text-decoration:none;font-weight:700;">Start 7-day free trial</a>' +
822
+ '<div style="color:#666;font-size:12px;margin-top:10px;">npx thumbgate pro --activate --key=YOUR_KEY</div>' +
823
+ '</div></div>';
824
+ el.appendChild(wall);
825
+ }
826
+
827
+ function loadDemo() {
828
+ allRules = demoRules.slice(0, 3);
829
+ allTimeline = demoTimeline.slice(0, 3);
830
+ dashboardSnapshot = null;
831
+ activeTimelineSignal = 'all';
832
+ activeTimelineDay = null;
833
+
834
+ document.getElementById('statRules').textContent = demoRules.length;
835
+ document.getElementById('statCritical').textContent = demoRules.filter(function(r) { return r.severity === 'critical'; }).length;
836
+ document.getElementById('statBlocked').textContent = demoRules.reduce(function(sum, r) { return sum + (r.prevented || 0); }, 0);
837
+ document.getElementById('statTrend').textContent = '12.5%';
838
+ document.getElementById('statTrendSub').textContent = '7-day approval rate';
839
+
840
+ renderRules(allRules);
841
+ renderTimeline(allTimeline);
842
+ renderInsights();
843
+ renderImprovementMetrics({ recentRate: 0.125 }, null);
844
+ renderUpgradeWall('rulesList');
845
+ renderUpgradeWall('timelineList');
846
+ setMode('Demo preview — upgrade to Pro to see your live lessons, timeline, and insights.', true);
847
+ document.getElementById('proBadge').style.display = 'block';
848
+ }
849
+
850
+ async function loadLive() {
851
+ if (!hasBootstrapKey()) {
852
+ loadDemo();
853
+ return;
854
+ }
855
+ API_KEY = String(BOOTSTRAP_API_KEY || '').trim();
856
+ try {
857
+ const responses = await Promise.all([
858
+ fetch('/v1/feedback/stats', { headers: getHeaders() }),
859
+ fetch('/v1/lessons/search?limit=50', { headers: getHeaders() }),
860
+ fetch('/v1/search?q=*&limit=40&source=feedback', { headers: getHeaders() }),
861
+ fetch('/v1/dashboard', { headers: getHeaders() })
862
+ ]);
863
+ if (responses.some(function(res) { return !res.ok; })) {
864
+ throw new Error('Local Pro bootstrap unavailable');
865
+ }
866
+
867
+ const stats = await responses[0].json();
868
+ const lessonsPayload = await responses[1].json();
869
+ const feedbackPayload = await responses[2].json();
870
+ dashboardSnapshot = await responses[3].json();
871
+
872
+ allRules = mapLiveRules(lessonsPayload);
873
+ var lessonMap = new Map(allRules
874
+ .filter(function(rule) { return rule.sourceFeedbackId; })
875
+ .map(function(rule) { return [rule.sourceFeedbackId, rule]; }));
876
+ allTimeline = mapTimelineItems(feedbackPayload, lessonMap);
877
+ activeTimelineSignal = 'all';
878
+ activeTimelineDay = null;
879
+
880
+ document.getElementById('statRules').textContent = allRules.length;
881
+ document.getElementById('statCritical').textContent = allRules.filter(function(r) { return r.severity === 'critical'; }).length;
882
+ document.getElementById('statBlocked').textContent = ((dashboardSnapshot.gateStats || {}).blocked || 0);
883
+ document.getElementById('statTrend').textContent = ((stats.recentRate || stats.approvalRate || 0) * 100).toFixed(1) + '%';
884
+ document.getElementById('statTrendSub').textContent = 'Live approval rate';
885
+
886
+ renderRules(allRules);
887
+ renderTimeline(allTimeline);
888
+ renderInsights(dashboardSnapshot);
889
+ renderImprovementMetrics(stats, dashboardSnapshot);
890
+ document.getElementById('proBadge').style.display = 'none';
891
+ setMode('Local Pro connected — this lessons view is reading your live rules, feedback timeline, and insights.', false);
892
+ } catch (_err) {
893
+ loadDemo();
894
+ }
895
+ }
896
+
897
+ loadLive().then(function() {
898
+ // Default: highlight Active Rules card on page load
899
+ highlightCard(0);
900
+
901
+ // Handle #hash — scroll to and highlight the matching item.
902
+ // IMPORTANT: querySelector finds elements in hidden tabs (display:none),
903
+ // but scrollIntoView silently fails on hidden elements. Always switch to
904
+ // the correct tab BEFORE querying.
905
+ var hash = window.location.hash.replace('#', '');
906
+ if (!hash) return;
907
+ var el = null;
908
+
909
+ // For fb_* hashes (from statusbar links), prefer rules tab (has lesson context)
910
+ if (hash.indexOf('fb_') === 0 || hash.indexOf('lesson_') === 0) {
911
+ var matchedRule = allRules.find(function(r) {
912
+ return r.sourceFeedbackId === hash || r.id === hash;
913
+ });
914
+ if (matchedRule) {
915
+ switchTab('rules');
916
+ el = document.querySelector('[data-rule-id="' + matchedRule.id + '"]');
917
+ }
918
+ if (!el) {
919
+ switchTab('timeline');
920
+ el = document.querySelector('[data-feedback-id="' + hash + '"]');
921
+ }
922
+ }
923
+
924
+ // For mem_* or other IDs, search within visible tabs only
925
+ if (!el) {
926
+ switchTab('rules');
927
+ el = document.querySelector('#tab-rules [data-rule-id="' + hash + '"]');
928
+ }
929
+ if (!el) {
930
+ switchTab('timeline');
931
+ el = document.querySelector('#tab-timeline [data-feedback-id="' + hash + '"]');
932
+ }
933
+
934
+ if (el) {
935
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
936
+ el.style.borderColor = 'var(--cyan)';
937
+ el.style.boxShadow = '0 0 12px rgba(34,211,238,0.3)';
938
+ setTimeout(function() { el.style.borderColor = ''; el.style.boxShadow = ''; }, 4000);
939
+ }
940
+ });
941
+ </script>
942
+
943
+ <div class="feedback-widget">
944
+ <div class="feedback-panel" id="feedbackPanel">
945
+ <h3>Share feedback with ThumbGate team</h3>
946
+ <select id="feedbackCategory">
947
+ <option value="bug">Bug report</option>
948
+ <option value="feature">Feature request</option>
949
+ <option value="question">Question</option>
950
+ </select>
951
+ <textarea id="feedbackText" placeholder="What's on your mind? Be honest — we read every one."></textarea>
952
+ <button class="feedback-submit" id="feedbackSubmit" onclick="submitFeedback()">Submit</button>
953
+ <div id="feedbackResult"></div>
954
+ </div>
955
+ <button class="feedback-toggle" onclick="toggleFeedback()">💬 Feedback</button>
956
+ </div>
957
+ <script>
958
+ function toggleFeedback() {
959
+ document.getElementById('feedbackPanel').classList.toggle('open');
960
+ document.getElementById('feedbackResult').innerHTML = '';
961
+ }
962
+ async function submitFeedback() {
963
+ var btn = document.getElementById('feedbackSubmit');
964
+ var text = document.getElementById('feedbackText').value.trim();
965
+ var category = document.getElementById('feedbackCategory').value;
966
+ if (!text) return;
967
+ btn.disabled = true; btn.textContent = 'Submitting...';
968
+ try {
969
+ var res = await fetch('/api/feedback/submit', {
970
+ method: 'POST',
971
+ headers: { 'Content-Type': 'application/json' },
972
+ body: JSON.stringify({ category: category, message: text })
973
+ });
974
+ var data = await res.json();
975
+ if (data.success) {
976
+ document.getElementById('feedbackResult').innerHTML = '<div class="feedback-success">Thanks! Filed as <a href="' + data.issueUrl + '" target="_blank" style="color:var(--cyan)">#' + data.issueNumber + '</a></div>';
977
+ document.getElementById('feedbackText').value = '';
978
+ } else {
979
+ document.getElementById('feedbackResult').innerHTML = '<div style="color:var(--red);font-size:13px">Failed: ' + (data.error || 'unknown') + '</div>';
980
+ }
981
+ } catch (e) {
982
+ document.getElementById('feedbackResult').innerHTML = '<div style="color:var(--red);font-size:13px">Network error. Try again.</div>';
983
+ }
984
+ btn.disabled = false; btn.textContent = 'Submit';
985
+ }
986
+ </script>
987
+
988
+ </body>
989
+ </html>