thumbgate 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/README.md +4 -4
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +35 -14
- package/adapters/README.md +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +2 -2
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +20 -11
- package/config/github-about.json +1 -1
- package/config/model-tiers.json +11 -0
- package/package.json +8 -6
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +1 -1
- package/plugins/codex-profile/INSTALL.md +1 -1
- package/plugins/codex-profile/README.md +1 -1
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/cursor-marketplace/README.md +2 -2
- package/plugins/cursor-marketplace/commands/capture-feedback.md +2 -2
- package/plugins/cursor-marketplace/rules/feedback-capture.mdc +3 -3
- package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +3 -2
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/compare.html +4 -4
- package/public/guide.html +4 -4
- package/public/index.html +51 -38
- package/public/learn/ai-agent-persistent-memory.html +1 -0
- package/public/lessons.html +325 -17
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/audit-trail.js +6 -0
- package/scripts/capture-railway-diagnostics.sh +97 -0
- package/scripts/check-congruence.js +1 -1
- package/scripts/claude-feedback-sync.js +320 -0
- package/scripts/cli-telemetry.js +4 -1
- package/scripts/contextfs.js +32 -23
- package/scripts/dashboard.js +84 -0
- package/scripts/feedback-loop.js +16 -0
- package/scripts/intervention-policy.js +696 -0
- package/scripts/local-model-profile.js +18 -2
- package/scripts/model-tier-router.js +10 -1
- package/scripts/operational-integrity.js +354 -31
- package/scripts/prove-adapters.js +1 -0
- package/scripts/prove-automation.js +2 -2
- package/scripts/prove-packaged-runtime.js +260 -0
- package/scripts/prove-runtime.js +13 -0
- package/scripts/rate-limiter.js +3 -3
- package/scripts/statusline-local-stats.js +2 -0
- package/scripts/statusline.sh +166 -11
- package/scripts/tool-registry.js +2 -2
- package/scripts/workflow-sentinel.js +114 -4
- package/skills/thumbgate/SKILL.md +1 -1
package/public/lessons.html
CHANGED
|
@@ -42,6 +42,49 @@
|
|
|
42
42
|
.stat-value.purple { color: var(--purple); }
|
|
43
43
|
.stat-sub { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
|
|
44
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
|
+
|
|
45
88
|
/* TABS */
|
|
46
89
|
.tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 24px; }
|
|
47
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; }
|
|
@@ -114,6 +157,9 @@
|
|
|
114
157
|
|
|
115
158
|
@media (max-width: 700px) {
|
|
116
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; }
|
|
117
163
|
.filter-row { flex-wrap: wrap; }
|
|
118
164
|
.rule-meta { flex-direction: column; gap: 8px; }
|
|
119
165
|
}
|
|
@@ -152,7 +198,7 @@
|
|
|
152
198
|
<div class="container">
|
|
153
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;">
|
|
154
200
|
<h1 style="font-size:22px;font-weight:700;margin-bottom:8px;letter-spacing:-0.02em;">📚 Lessons Learned</h1>
|
|
155
|
-
<p style="font-size:14px;color:var(--text-muted);line-height:1.6;max-width:700px;">
|
|
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>
|
|
156
202
|
<div style="display:flex;gap:16px;margin-top:12px;font-size:12px;color:var(--text-muted);">
|
|
157
203
|
<span>📋 <strong style="color:var(--text);">Active Rules</strong> — what was learned</span>
|
|
158
204
|
<span>📊 <strong style="color:var(--text);">Timeline</strong> — when it was learned</span>
|
|
@@ -171,9 +217,9 @@
|
|
|
171
217
|
<div class="stat-sub">Highest severity rules</div>
|
|
172
218
|
</div>
|
|
173
219
|
<div class="stat-card" onclick="switchTab('timeline')">
|
|
174
|
-
<div class="stat-label">
|
|
175
|
-
<div class="stat-value green" id="
|
|
176
|
-
<div class="stat-sub">
|
|
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>
|
|
177
223
|
</div>
|
|
178
224
|
<div class="stat-card" onclick="switchTab('insights')">
|
|
179
225
|
<div class="stat-label">Approval Trend</div>
|
|
@@ -186,6 +232,54 @@
|
|
|
186
232
|
Loading lessons view...
|
|
187
233
|
</div>
|
|
188
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">7d approval</div>
|
|
246
|
+
<div class="metric-number purple">--</div>
|
|
247
|
+
<div class="metric-note">Waiting for live feedback</div>
|
|
248
|
+
</div>
|
|
249
|
+
<div class="metric-tile">
|
|
250
|
+
<div class="metric-kicker">This week negatives</div>
|
|
251
|
+
<div class="metric-number red">--</div>
|
|
252
|
+
<div class="metric-note">Waiting for live feedback</div>
|
|
253
|
+
</div>
|
|
254
|
+
<div class="metric-tile">
|
|
255
|
+
<div class="metric-kicker">Actions blocked</div>
|
|
256
|
+
<div class="metric-number green">--</div>
|
|
257
|
+
<div class="metric-note">Recorded gate denies</div>
|
|
258
|
+
</div>
|
|
259
|
+
<div class="metric-tile">
|
|
260
|
+
<div class="metric-kicker">Repeat pressure</div>
|
|
261
|
+
<div class="metric-number cyan">--</div>
|
|
262
|
+
<div class="metric-note">Share of negatives that repeat</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
|
+
|
|
189
283
|
<div class="tabs">
|
|
190
284
|
<div class="tab active" onclick="switchTab('rules')">📋 Active Rules</div>
|
|
191
285
|
<div class="tab" onclick="switchTab('timeline')">📊 Feedback Timeline</div>
|
|
@@ -224,13 +318,13 @@
|
|
|
224
318
|
<!-- TAB 3: INSIGHTS (PRO) -->
|
|
225
319
|
<div class="tab-content" id="tab-insights">
|
|
226
320
|
<div class="insight-card">
|
|
227
|
-
<h3>🔥 Top Recurring
|
|
321
|
+
<h3>🔥 Top Recurring Failure Patterns</h3>
|
|
228
322
|
<ul class="insight-list" id="insightMistakes">
|
|
229
323
|
<li><span>Loading...</span></li>
|
|
230
324
|
</ul>
|
|
231
325
|
</div>
|
|
232
326
|
<div class="insight-card">
|
|
233
|
-
<h3>🛡️ Most
|
|
327
|
+
<h3>🛡️ Most Reinforced Rules</h3>
|
|
234
328
|
<ul class="insight-list" id="insightEffective">
|
|
235
329
|
<li><span>Loading...</span></li>
|
|
236
330
|
</ul>
|
|
@@ -243,7 +337,7 @@
|
|
|
243
337
|
</div>
|
|
244
338
|
<div class="pro-badge" id="proBadge" style="display:none;">
|
|
245
339
|
<h3>Unlock Full Insights</h3>
|
|
246
|
-
<p>DPO training data export, effectiveness charts, pattern clustering,
|
|
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>
|
|
247
341
|
<a href="/#pricing">Get Pro — $19/mo</a>
|
|
248
342
|
</div>
|
|
249
343
|
</div>
|
|
@@ -255,10 +349,14 @@ var allTimeline = [];
|
|
|
255
349
|
var isDemo = true;
|
|
256
350
|
var API_KEY = '';
|
|
257
351
|
var dashboardSnapshot = null;
|
|
352
|
+
var activeTimelineSignal = 'all';
|
|
353
|
+
var activeTimelineDay = null;
|
|
354
|
+
var currentImprovementSeries = [];
|
|
258
355
|
const BOOTSTRAP_API_KEY = __LESSONS_BOOTSTRAP_KEY__;
|
|
259
356
|
const LOCAL_PRO_BOOTSTRAP = __LESSONS_BOOTSTRAP_ENABLED__;
|
|
260
357
|
|
|
261
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, '"'); }
|
|
262
360
|
|
|
263
361
|
function getHeaders() {
|
|
264
362
|
return { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' };
|
|
@@ -284,6 +382,29 @@ function formatDate(value) {
|
|
|
284
382
|
return ts.toLocaleString(undefined, { year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: '2-digit', second: '2-digit', timeZoneName: 'short' });
|
|
285
383
|
}
|
|
286
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
|
+
|
|
287
408
|
function highlightCard(index) {
|
|
288
409
|
document.querySelectorAll('.stat-card').forEach(function(c, i) {
|
|
289
410
|
c.classList.toggle('selected', i === index);
|
|
@@ -325,12 +446,54 @@ function filterSeverity(level, el) {
|
|
|
325
446
|
}
|
|
326
447
|
|
|
327
448
|
function filterTimeline(signal, el) {
|
|
449
|
+
activeTimelineSignal = signal;
|
|
328
450
|
if (el) {
|
|
329
451
|
document.querySelectorAll('#tab-timeline .filter-btn').forEach(function(b) { b.classList.remove('active'); });
|
|
330
452
|
el.classList.add('active');
|
|
331
453
|
}
|
|
332
|
-
|
|
333
|
-
|
|
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.';
|
|
496
|
+
}
|
|
334
497
|
}
|
|
335
498
|
|
|
336
499
|
function searchRules() {
|
|
@@ -362,7 +525,7 @@ function renderRules(rules) {
|
|
|
362
525
|
'</div>' +
|
|
363
526
|
(r.context ? '<div class="rule-context">' + escHtml(r.context) + '</div>' : '') +
|
|
364
527
|
'<div class="rule-meta">' +
|
|
365
|
-
'<div class="rule-meta-item">
|
|
528
|
+
'<div class="rule-meta-item">Seen in feedback <span class="value">' + (r.occurrences || 0) + '</span> times</div>' +
|
|
366
529
|
(r.lastTriggered ? '<div class="rule-meta-item">Last: <span class="value">' + escHtml(r.lastTriggered) + '</span></div>' : '') +
|
|
367
530
|
'</div>' +
|
|
368
531
|
(r.tags && r.tags.length ? '<div class="rule-tags">' + r.tags.map(function(t) { return '<span class="tag">' + escHtml(t) + '</span>'; }).join('') + '</div>' : '') +
|
|
@@ -420,17 +583,154 @@ function renderInsights(data) {
|
|
|
420
583
|
|
|
421
584
|
mistakes.innerHTML = topMistakes.length ? topMistakes.map(function(r) {
|
|
422
585
|
return insightRow(r, '<span style="color:var(--red);font-weight:600">' + (r.occurrences || 0) + 'x</span>');
|
|
423
|
-
}).join('') : '<li><span style="color:var(--green)">No critical
|
|
586
|
+
}).join('') : '<li><span style="color:var(--green)">No critical repeated failures this week</span></li>';
|
|
424
587
|
|
|
425
588
|
effective.innerHTML = topEffective.length ? topEffective.map(function(r) {
|
|
426
|
-
return insightRow(r, '<span class="rule-effectiveness">
|
|
427
|
-
}).join('') : '<li><span>No
|
|
589
|
+
return insightRow(r, '<span class="rule-effectiveness">Seen ' + (r.occurrences || 0) + 'x</span>');
|
|
590
|
+
}).join('') : '<li><span>No reinforced rules yet</span></li>';
|
|
428
591
|
|
|
429
592
|
stale.innerHTML = staleRules.length ? staleRules.map(function(r) {
|
|
430
593
|
return insightRow(r, '<span style="color:var(--text-muted)">Archive?</span>');
|
|
431
594
|
}).join('') : '<li><span style="color:var(--green)">All rules are active</span></li>';
|
|
432
595
|
}
|
|
433
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 chart is based on recorded feedback, gate denials, and repeat-failure pressure.';
|
|
631
|
+
grid.innerHTML = [
|
|
632
|
+
{ label: '7d approval', value: '12.5%', tone: 'purple', note: 'Demo preview only' },
|
|
633
|
+
{ label: 'This week negatives', value: '4', tone: 'red', note: 'Sample trend only' },
|
|
634
|
+
{ label: 'Actions blocked', value: '42', tone: 'green', note: 'Sample gate denies' },
|
|
635
|
+
{ label: 'Repeat pressure', value: '38%', tone: 'cyan', note: 'Sample repeated failures' }
|
|
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 harness = (data && data.harness) || {};
|
|
649
|
+
var approvalRate = Math.round(((stats && (stats.recentRate || stats.approvalRate)) || 0) * 100);
|
|
650
|
+
var errorTrend = liveMetrics.errorTrend || {};
|
|
651
|
+
var repeatPressure = Math.round(((harness.repeatFailureRate || 0) * 100));
|
|
652
|
+
var repeatedCount = harness.repeatedFailureCount || 0;
|
|
653
|
+
var thisWeekNeg = errorTrend.thisWeek || 0;
|
|
654
|
+
var lastWeekNeg = errorTrend.lastWeek || 0;
|
|
655
|
+
var blocked = gateStats.blocked || 0;
|
|
656
|
+
|
|
657
|
+
summary.textContent = '7-day approval is ' + approvalRate + '%. This week logged ' + thisWeekNeg + ' negative signal' + (thisWeekNeg === 1 ? '' : 's')
|
|
658
|
+
+ ' versus ' + lastWeekNeg + ' last week, while gates recorded ' + blocked + ' deny decision' + (blocked === 1 ? '' : 's') + '.';
|
|
659
|
+
|
|
660
|
+
grid.innerHTML = [
|
|
661
|
+
{ label: '7d approval', value: approvalRate + '%', tone: 'purple', note: 'Recent approval rate from feedback signals' },
|
|
662
|
+
{ label: 'This week negatives', value: String(thisWeekNeg), tone: 'red', note: 'Vs ' + lastWeekNeg + ' last week' },
|
|
663
|
+
{ label: 'Actions blocked', value: String(blocked), tone: 'green', note: 'Recorded gate denies from the audit trail' },
|
|
664
|
+
{ label: 'Repeat pressure', value: repeatPressure + '%', tone: 'cyan', note: repeatedCount + ' repeated failure pattern' + (repeatedCount === 1 ? '' : 's') }
|
|
665
|
+
].map(function(tile) {
|
|
666
|
+
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>';
|
|
667
|
+
}).join('');
|
|
668
|
+
|
|
669
|
+
var gateDays = {};
|
|
670
|
+
((gateAudit.days) || []).forEach(function(day) {
|
|
671
|
+
gateDays[day.dayKey] = day;
|
|
672
|
+
});
|
|
673
|
+
var series = buildFeedbackSeries(allTimeline, 14).map(function(item) {
|
|
674
|
+
var gateDay = gateDays[item.dayKey] || { deny: 0, warn: 0, intercepted: 0 };
|
|
675
|
+
return {
|
|
676
|
+
dayKey: item.dayKey,
|
|
677
|
+
label: item.label,
|
|
678
|
+
up: item.up,
|
|
679
|
+
down: item.down,
|
|
680
|
+
total: item.total,
|
|
681
|
+
deny: gateDay.deny || 0,
|
|
682
|
+
warn: gateDay.warn || 0,
|
|
683
|
+
intercepted: gateDay.intercepted || 0
|
|
684
|
+
};
|
|
685
|
+
});
|
|
686
|
+
currentImprovementSeries = series;
|
|
687
|
+
var maxFeedbackTotal = series.reduce(function(max, item) { return Math.max(max, item.total); }, 0);
|
|
688
|
+
var maxGateTotal = series.reduce(function(max, item) { return Math.max(max, item.intercepted); }, 0);
|
|
689
|
+
if (maxFeedbackTotal === 0 && maxGateTotal === 0) {
|
|
690
|
+
currentImprovementSeries = [];
|
|
691
|
+
chart.innerHTML = '<div class="empty" style="grid-column:1 / -1;">No recent feedback events to chart yet.</div>';
|
|
692
|
+
updateMetricsSelection();
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
chart.innerHTML = series.map(function(item) {
|
|
697
|
+
if (!item.total && !item.intercepted) {
|
|
698
|
+
return '<button class="metrics-bar" type="button" data-day-key="' + escAttr(item.dayKey) + '" onclick="focusTimelineDay(\'' + escAttr(item.dayKey) + '\')">' +
|
|
699
|
+
'<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>' +
|
|
700
|
+
'<div class="metrics-bar-total">F0</div>' +
|
|
701
|
+
'<div class="metrics-bar-subtotal">G0</div>' +
|
|
702
|
+
'<div class="metrics-bar-label">' + escHtml(item.label) + '</div>' +
|
|
703
|
+
'</button>';
|
|
704
|
+
}
|
|
705
|
+
var feedbackHeight = item.total > 0 ? Math.max(14, Math.round((item.total / Math.max(maxFeedbackTotal, 1)) * 120)) : 0;
|
|
706
|
+
var upHeight = item.up > 0 ? Math.max(8, Math.round((item.up / Math.max(item.total, 1)) * feedbackHeight)) : 0;
|
|
707
|
+
var downHeight = item.down > 0 ? Math.max(8, feedbackHeight - upHeight) : 0;
|
|
708
|
+
if (item.up > 0 && item.down > 0 && upHeight + downHeight > 120) downHeight = Math.max(8, 120 - upHeight);
|
|
709
|
+
var gateHeight = item.intercepted > 0 ? Math.max(12, Math.round((item.intercepted / Math.max(maxGateTotal, 1)) * 84)) : 0;
|
|
710
|
+
var denyHeight = item.deny > 0 ? Math.max(8, Math.round((item.deny / Math.max(item.intercepted, 1)) * gateHeight)) : 0;
|
|
711
|
+
var warnHeight = item.warn > 0 ? Math.max(8, gateHeight - denyHeight) : 0;
|
|
712
|
+
if (item.deny > 0 && item.warn > 0 && denyHeight + warnHeight > 84) warnHeight = Math.max(8, 84 - denyHeight);
|
|
713
|
+
return '<button class="metrics-bar" type="button" data-day-key="' + escAttr(item.dayKey) + '" onclick="focusTimelineDay(\'' + escAttr(item.dayKey) + '\')">' +
|
|
714
|
+
'<div class="metrics-bar-body">' +
|
|
715
|
+
'<div class="metrics-bar-stack">' +
|
|
716
|
+
(item.up ? '<div class="metrics-segment up" style="height:' + upHeight + 'px" title="' + item.up + ' positive feedback"></div>' : '') +
|
|
717
|
+
(item.down ? '<div class="metrics-segment down" style="height:' + downHeight + 'px" title="' + item.down + ' negative feedback"></div>' : '') +
|
|
718
|
+
(!item.total ? '<div class="metrics-bar-empty"></div>' : '') +
|
|
719
|
+
'</div>' +
|
|
720
|
+
'<div class="metrics-bar-stack gate">' +
|
|
721
|
+
(item.deny ? '<div class="metrics-segment deny" style="height:' + denyHeight + 'px" title="' + item.deny + ' gate denies"></div>' : '') +
|
|
722
|
+
(item.warn ? '<div class="metrics-segment warn" style="height:' + warnHeight + 'px" title="' + item.warn + ' gate warns"></div>' : '') +
|
|
723
|
+
(!item.intercepted ? '<div class="metrics-bar-empty"></div>' : '') +
|
|
724
|
+
'</div>' +
|
|
725
|
+
'</div>' +
|
|
726
|
+
'<div class="metrics-bar-total">F' + item.total + '</div>' +
|
|
727
|
+
'<div class="metrics-bar-subtotal">G' + item.intercepted + '</div>' +
|
|
728
|
+
'<div class="metrics-bar-label">' + escHtml(item.label) + '</div>' +
|
|
729
|
+
'</button>';
|
|
730
|
+
}).join('');
|
|
731
|
+
updateMetricsSelection();
|
|
732
|
+
}
|
|
733
|
+
|
|
434
734
|
function searchAndHighlight(ruleId) {
|
|
435
735
|
var el = document.querySelector('[data-rule-id="' + ruleId + '"]');
|
|
436
736
|
if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); el.style.borderColor = 'var(--cyan)'; setTimeout(function() { el.style.borderColor = ''; }, 2000); }
|
|
@@ -452,7 +752,6 @@ function mapLiveRules(payload) {
|
|
|
452
752
|
title: result.title || ((result.lesson || {}).summary) || 'Lesson',
|
|
453
753
|
context: ((result.lesson || {}).howToAvoid) || ((result.lesson || {}).summary) || (sourceFeedback && sourceFeedback.context) || '',
|
|
454
754
|
occurrences: linkedGate && linkedGate.occurrences ? linkedGate.occurrences : 1,
|
|
455
|
-
prevented: linkedGate && linkedGate.occurrences ? linkedGate.occurrences : 0,
|
|
456
755
|
severity: deriveSeverity(result),
|
|
457
756
|
tags: Array.isArray(result.tags) ? result.tags : [],
|
|
458
757
|
lastTriggered: sourceFeedback && sourceFeedback.timestamp ? formatDate(sourceFeedback.timestamp) : formatDate(result.timestamp),
|
|
@@ -464,10 +763,13 @@ function mapLiveRules(payload) {
|
|
|
464
763
|
function mapTimelineItems(payload, lessonMap) {
|
|
465
764
|
return (payload.results || []).map(function(entry) {
|
|
466
765
|
var linkedLesson = lessonMap.get(entry.id) || null;
|
|
766
|
+
var timestamp = entry.timestamp || null;
|
|
467
767
|
return {
|
|
468
|
-
signal: entry.signal ||
|
|
768
|
+
signal: normalizeSignal(entry.signal || entry.feedback),
|
|
469
769
|
context: entry.context || entry.title || '',
|
|
470
770
|
date: formatDate(entry.timestamp),
|
|
771
|
+
timestamp: timestamp,
|
|
772
|
+
dayKey: toDayKey(timestamp),
|
|
471
773
|
learned: linkedLesson ? linkedLesson.title : '',
|
|
472
774
|
feedbackId: entry.id || ''
|
|
473
775
|
};
|
|
@@ -517,16 +819,19 @@ function loadDemo() {
|
|
|
517
819
|
allRules = demoRules.slice(0, 3);
|
|
518
820
|
allTimeline = demoTimeline.slice(0, 3);
|
|
519
821
|
dashboardSnapshot = null;
|
|
822
|
+
activeTimelineSignal = 'all';
|
|
823
|
+
activeTimelineDay = null;
|
|
520
824
|
|
|
521
825
|
document.getElementById('statRules').textContent = demoRules.length;
|
|
522
826
|
document.getElementById('statCritical').textContent = demoRules.filter(function(r) { return r.severity === 'critical'; }).length;
|
|
523
|
-
document.getElementById('
|
|
827
|
+
document.getElementById('statBlocked').textContent = demoRules.reduce(function(sum, r) { return sum + (r.prevented || 0); }, 0);
|
|
524
828
|
document.getElementById('statTrend').textContent = '12.5%';
|
|
525
829
|
document.getElementById('statTrendSub').textContent = '7-day approval rate';
|
|
526
830
|
|
|
527
831
|
renderRules(allRules);
|
|
528
832
|
renderTimeline(allTimeline);
|
|
529
833
|
renderInsights();
|
|
834
|
+
renderImprovementMetrics({ recentRate: 0.125 }, null);
|
|
530
835
|
renderUpgradeWall('rulesList');
|
|
531
836
|
renderUpgradeWall('timelineList');
|
|
532
837
|
setMode('Demo preview — upgrade to Pro to see your live lessons, timeline, and insights.', true);
|
|
@@ -560,16 +865,19 @@ async function loadLive() {
|
|
|
560
865
|
.filter(function(rule) { return rule.sourceFeedbackId; })
|
|
561
866
|
.map(function(rule) { return [rule.sourceFeedbackId, rule]; }));
|
|
562
867
|
allTimeline = mapTimelineItems(feedbackPayload, lessonMap);
|
|
868
|
+
activeTimelineSignal = 'all';
|
|
869
|
+
activeTimelineDay = null;
|
|
563
870
|
|
|
564
871
|
document.getElementById('statRules').textContent = allRules.length;
|
|
565
872
|
document.getElementById('statCritical').textContent = allRules.filter(function(r) { return r.severity === 'critical'; }).length;
|
|
566
|
-
document.getElementById('
|
|
873
|
+
document.getElementById('statBlocked').textContent = ((dashboardSnapshot.gateStats || {}).blocked || 0);
|
|
567
874
|
document.getElementById('statTrend').textContent = ((stats.recentRate || stats.approvalRate || 0) * 100).toFixed(1) + '%';
|
|
568
875
|
document.getElementById('statTrendSub').textContent = 'Live approval rate';
|
|
569
876
|
|
|
570
877
|
renderRules(allRules);
|
|
571
878
|
renderTimeline(allTimeline);
|
|
572
879
|
renderInsights(dashboardSnapshot);
|
|
880
|
+
renderImprovementMetrics(stats, dashboardSnapshot);
|
|
573
881
|
document.getElementById('proBadge').style.display = 'none';
|
|
574
882
|
setMode('Local Pro connected — this lessons view is reading your live rules, feedback timeline, and insights.', false);
|
|
575
883
|
} catch (_err) {
|
|
Binary file
|
package/scripts/audit-trail.js
CHANGED
|
@@ -64,6 +64,12 @@ function recordAuditEvent(params = {}) {
|
|
|
64
64
|
};
|
|
65
65
|
|
|
66
66
|
fs.appendFileSync(logPath, JSON.stringify(record) + '\n');
|
|
67
|
+
try {
|
|
68
|
+
const { trainAndPersistInterventionPolicy } = require('./intervention-policy');
|
|
69
|
+
trainAndPersistInterventionPolicy(path.dirname(logPath));
|
|
70
|
+
} catch {
|
|
71
|
+
// Keep audit recording resilient even if the learned policy refresh fails.
|
|
72
|
+
}
|
|
67
73
|
return record;
|
|
68
74
|
}
|
|
69
75
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
DIAG_DIR="${1:-railway-diagnostics}"
|
|
5
|
+
LOG_LINES="${RAILWAY_LOG_LINES:-200}"
|
|
6
|
+
HTTP_LOG_LINES="${RAILWAY_HTTP_LOG_LINES:-50}"
|
|
7
|
+
CONNECT_TIMEOUT_SECONDS="${RAILWAY_HEALTHCHECK_CONNECT_TIMEOUT_SECONDS:-5}"
|
|
8
|
+
MAX_TIME_SECONDS="${RAILWAY_HEALTHCHECK_MAX_TIME_SECONDS:-20}"
|
|
9
|
+
|
|
10
|
+
mkdir -p "$DIAG_DIR"
|
|
11
|
+
|
|
12
|
+
if ! command -v railway >/dev/null 2>&1; then
|
|
13
|
+
echo 'Railway CLI is not installed; skipping Railway diagnostics capture.'
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
if [ -z "${RAILWAY_PROJECT_ID:-}" ] || [ -z "${RAILWAY_ENVIRONMENT_ID:-}" ]; then
|
|
18
|
+
echo 'RAILWAY_PROJECT_ID and RAILWAY_ENVIRONMENT_ID are required for Railway diagnostics.'
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
SERVICE_ARGS=()
|
|
23
|
+
if [ -n "${RAILWAY_SERVICE:-}" ]; then
|
|
24
|
+
SERVICE_ARGS+=(--service "$RAILWAY_SERVICE")
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
ENV_ARGS=(--environment "$RAILWAY_ENVIRONMENT_ID")
|
|
28
|
+
|
|
29
|
+
run_capture() {
|
|
30
|
+
local label="$1"
|
|
31
|
+
local output_path="$2"
|
|
32
|
+
shift 2
|
|
33
|
+
|
|
34
|
+
echo "== ${label} =="
|
|
35
|
+
if "$@" >"$output_path" 2>"${output_path}.stderr"; then
|
|
36
|
+
sed -n '1,120p' "$output_path" || true
|
|
37
|
+
return 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
sed -n '1,120p' "${output_path}.stderr" || true
|
|
41
|
+
return 1
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
run_capture \
|
|
45
|
+
'railway link' \
|
|
46
|
+
"$DIAG_DIR/link.json" \
|
|
47
|
+
railway link --project "$RAILWAY_PROJECT_ID" "${ENV_ARGS[@]}" "${SERVICE_ARGS[@]}" --json || true
|
|
48
|
+
|
|
49
|
+
run_capture \
|
|
50
|
+
'railway status' \
|
|
51
|
+
"$DIAG_DIR/status.json" \
|
|
52
|
+
railway status --json || true
|
|
53
|
+
|
|
54
|
+
run_capture \
|
|
55
|
+
'railway service status' \
|
|
56
|
+
"$DIAG_DIR/service-status.json" \
|
|
57
|
+
railway service status "${SERVICE_ARGS[@]}" "${ENV_ARGS[@]}" --json || true
|
|
58
|
+
|
|
59
|
+
run_capture \
|
|
60
|
+
'railway deployment logs' \
|
|
61
|
+
"$DIAG_DIR/deployment-logs.jsonl" \
|
|
62
|
+
railway logs "${SERVICE_ARGS[@]}" "${ENV_ARGS[@]}" --latest --deployment --lines "$LOG_LINES" --json || true
|
|
63
|
+
|
|
64
|
+
run_capture \
|
|
65
|
+
'railway build logs' \
|
|
66
|
+
"$DIAG_DIR/build-logs.jsonl" \
|
|
67
|
+
railway logs "${SERVICE_ARGS[@]}" "${ENV_ARGS[@]}" --latest --build --lines "$LOG_LINES" --json || true
|
|
68
|
+
|
|
69
|
+
run_capture \
|
|
70
|
+
'railway http logs (/health 5xx)' \
|
|
71
|
+
"$DIAG_DIR/http-health-5xx.jsonl" \
|
|
72
|
+
railway logs "${SERVICE_ARGS[@]}" "${ENV_ARGS[@]}" --latest --http --path /health --status '>=500' --lines "$HTTP_LOG_LINES" --json || true
|
|
73
|
+
|
|
74
|
+
run_capture \
|
|
75
|
+
'railway http logs (all 5xx)' \
|
|
76
|
+
"$DIAG_DIR/http-5xx.jsonl" \
|
|
77
|
+
railway logs "${SERVICE_ARGS[@]}" "${ENV_ARGS[@]}" --latest --http --status '>=500' --lines "$HTTP_LOG_LINES" --json || true
|
|
78
|
+
|
|
79
|
+
HEALTHCHECK_URL="${RAILWAY_HEALTHCHECK_URL:-}"
|
|
80
|
+
if [ -n "$HEALTHCHECK_URL" ]; then
|
|
81
|
+
echo '== direct health probe =='
|
|
82
|
+
HEALTH_STATUS="$(
|
|
83
|
+
curl \
|
|
84
|
+
--connect-timeout "$CONNECT_TIMEOUT_SECONDS" \
|
|
85
|
+
--max-time "$MAX_TIME_SECONDS" \
|
|
86
|
+
-sS \
|
|
87
|
+
-D "$DIAG_DIR/health-headers.txt" \
|
|
88
|
+
-o "$DIAG_DIR/health-body.txt" \
|
|
89
|
+
-w '%{http_code}' \
|
|
90
|
+
"$HEALTHCHECK_URL" || true
|
|
91
|
+
)"
|
|
92
|
+
printf '%s\n' "$HEALTH_STATUS" > "$DIAG_DIR/health-status.txt"
|
|
93
|
+
echo "Health status: ${HEALTH_STATUS:-<curl_failed>}"
|
|
94
|
+
if [ -s "$DIAG_DIR/health-body.txt" ]; then
|
|
95
|
+
sed -n '1,120p' "$DIAG_DIR/health-body.txt" || true
|
|
96
|
+
fi
|
|
97
|
+
fi
|
|
@@ -222,7 +222,7 @@ async function main() {
|
|
|
222
222
|
'public/index.html must mention the linked feedback session flow'
|
|
223
223
|
);
|
|
224
224
|
check(
|
|
225
|
-
/
|
|
225
|
+
/3 feedback captures\/day/i.test(landingHtml) && /5 lesson searches\/day/i.test(landingHtml),
|
|
226
226
|
'public/index.html must advertise the truthful free-tier capture and lesson-search limits'
|
|
227
227
|
);
|
|
228
228
|
check(
|