jekyll-theme-zer0 1.29.0 → 1.31.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.
Files changed (163) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +886 -2
  3. data/_data/README.md +2 -0
  4. data/_data/ai.yml +5 -3
  5. data/_data/ai_pricing.yml +36 -0
  6. data/_data/backlog.yml +196 -22
  7. data/_data/consumers.yml +147 -5
  8. data/_data/features.yml +147 -21
  9. data/_data/feedback_types.yml +17 -12
  10. data/_data/landing.yml +5 -2
  11. data/_data/navigation/quickstart.yml +4 -0
  12. data/_data/site_builder.yml +874 -0
  13. data/_data/theme-manifest.yml +132 -122
  14. data/_data/ui-text.yml +19 -0
  15. data/_includes/README.md +11 -1
  16. data/_includes/analytics/posthog.html +30 -11
  17. data/_includes/components/README.md +1 -1
  18. data/_includes/components/admin-links.html +2 -2
  19. data/_includes/components/admin-tabs.html +2 -2
  20. data/_includes/components/ai-chat.html +14 -11
  21. data/_includes/components/analytics-dashboard.html +8 -8
  22. data/_includes/components/author-bio.html +1 -1
  23. data/_includes/components/author-card.html +10 -2
  24. data/_includes/components/author-eeat.html +4 -4
  25. data/_includes/components/background-customizer.html +8 -8
  26. data/_includes/components/background-image.html +120 -0
  27. data/_includes/components/background-settings.html +28 -15
  28. data/_includes/components/collection-manager.html +5 -5
  29. data/_includes/components/component-showcase.html +13 -13
  30. data/_includes/components/config-editor.html +12 -12
  31. data/_includes/components/config-viewer.html +8 -8
  32. data/_includes/components/cookie-consent.html +41 -41
  33. data/_includes/components/cta-button.html +7 -2
  34. data/_includes/components/dev-shortcuts.html +7 -7
  35. data/_includes/components/env-dashboard.html +8 -8
  36. data/_includes/components/env-switcher.html +9 -9
  37. data/_includes/components/feature-card.html +2 -2
  38. data/_includes/components/halfmoon.html +2 -2
  39. data/_includes/components/info-section.html +36 -36
  40. data/_includes/components/js-cdn.html +15 -15
  41. data/_includes/components/language-toggle.html +4 -4
  42. data/_includes/components/mermaid.html +79 -435
  43. data/_includes/components/nanobar.html +5 -5
  44. data/_includes/components/nav-editor.html +2 -2
  45. data/_includes/components/nav-export.html +2 -2
  46. data/_includes/components/nav-overview.html +2 -2
  47. data/_includes/components/page-feedback.html +45 -30
  48. data/_includes/components/page-views-init.html +1 -1
  49. data/_includes/components/post-card.html +22 -22
  50. data/_includes/components/post-type-badge.html +2 -2
  51. data/_includes/components/powered-by.html +2 -2
  52. data/_includes/components/preview-image.html +28 -3
  53. data/_includes/components/quick-index.html +2 -2
  54. data/_includes/components/search-modal.html +27 -2
  55. data/_includes/components/searchbar.html +2 -2
  56. data/_includes/components/svg-background.html +2 -2
  57. data/_includes/components/theme-customizer.html +2 -2
  58. data/_includes/components/theme-info.html +6 -6
  59. data/_includes/components/theme-preview-gallery.html +23 -23
  60. data/_includes/content/giscus.html +2 -2
  61. data/_includes/content/intro.html +15 -8
  62. data/_includes/content/jsonld-faq.html +2 -2
  63. data/_includes/content/jsonld-software.html +2 -2
  64. data/_includes/content/seo.html +12 -4
  65. data/_includes/content/sitemap.html +247 -290
  66. data/_includes/content/toc.html +183 -183
  67. data/_includes/core/branding.html +13 -20
  68. data/_includes/core/console-capture.html +32 -74
  69. data/_includes/core/favicon.html +49 -7
  70. data/_includes/core/footer-fabs.html +17 -3
  71. data/_includes/core/footer.html +31 -18
  72. data/_includes/core/head.html +128 -92
  73. data/_includes/core/header.html +85 -54
  74. data/_includes/core/tokens-inline.html +16 -1
  75. data/_includes/docs/bootstrap-docs.html +8 -8
  76. data/_includes/landing/landing-install-cards.html +2 -2
  77. data/_includes/landing/landing-quick-links.html +1 -1
  78. data/_includes/navigation/admin-nav.html +2 -2
  79. data/_includes/navigation/nav-tree.html +8 -8
  80. data/_includes/navigation/navbar.html +21 -18
  81. data/_includes/navigation/section-sidebar.html +16 -16
  82. data/_includes/navigation/sidebar-config.html +36 -2
  83. data/_includes/navigation/sidebar-left.html +15 -15
  84. data/_includes/navigation/sidebar-right.html +6 -6
  85. data/_includes/obsidian/full-graph.html +2 -2
  86. data/_includes/setup/claude-session.html +72 -0
  87. data/_includes/setup/prereq-checklist.html +90 -0
  88. data/_includes/setup/wizard.html +910 -291
  89. data/_includes/stats/stats-categories.html +8 -8
  90. data/_includes/stats/stats-header.html +14 -14
  91. data/_includes/stats/stats-metrics.html +12 -12
  92. data/_includes/stats/stats-no-data.html +12 -12
  93. data/_includes/stats/stats-overview.html +6 -6
  94. data/_includes/stats/stats-tags.html +8 -8
  95. data/_layouts/404.html +38 -24
  96. data/_layouts/admin.html +22 -22
  97. data/_layouts/article.html +54 -37
  98. data/_layouts/author.html +20 -20
  99. data/_layouts/authors.html +2 -2
  100. data/_layouts/book-abc.html +12 -12
  101. data/_layouts/book-story.html +15 -15
  102. data/_layouts/book.html +12 -12
  103. data/_layouts/collection.html +33 -33
  104. data/_layouts/cookbook.html +12 -12
  105. data/_layouts/default.html +27 -24
  106. data/_layouts/home.html +36 -24
  107. data/_layouts/index.html +10 -10
  108. data/_layouts/landing.html +17 -17
  109. data/_layouts/news.html +44 -44
  110. data/_layouts/note.html +40 -39
  111. data/_layouts/notebook.html +36 -35
  112. data/_layouts/recipe.html +24 -24
  113. data/_layouts/root.html +95 -55
  114. data/_layouts/section.html +23 -23
  115. data/_layouts/setup.html +3 -3
  116. data/_layouts/sitemap-collection.html +49 -49
  117. data/_layouts/stats.html +40 -40
  118. data/_layouts/tag.html +12 -12
  119. data/_layouts/welcome.html +21 -21
  120. data/_sass/components/_cookie-banner.scss +48 -0
  121. data/_sass/components/_mermaid.scss +375 -0
  122. data/_sass/components/_setup-wizard.scss +588 -41
  123. data/_sass/components/_ui-enhancements.scss +11 -0
  124. data/_sass/core/_navbar.scss +147 -100
  125. data/_sass/core/_sidebar-extras.scss +2 -14
  126. data/_sass/core/code-copy.scss +27 -8
  127. data/_sass/layouts/_navbar-extras.scss +14 -4
  128. data/assets/css/extension-points-demo.css +27 -0
  129. data/assets/css/main.scss +1 -0
  130. data/assets/data/wiki-index.json +19 -0
  131. data/assets/js/ai-chat.js +47 -5
  132. data/assets/js/auto-hide-nav.js +53 -13
  133. data/assets/js/code-copy.js +72 -1
  134. data/assets/js/extension-points-demo.js +48 -0
  135. data/assets/js/fleet-feedback-capture.js +124 -0
  136. data/assets/js/fleet-feedback.js +853 -0
  137. data/assets/js/mermaid-diagrams.js +1373 -0
  138. data/assets/js/modules/navigation/config.js +9 -6
  139. data/assets/js/modules/navigation/scroll-spy.js +315 -80
  140. data/assets/js/modules/theme/appearance.js +8 -2
  141. data/assets/js/obsidian-local-graph.js +27 -5
  142. data/assets/js/obsidian-wiki-links.js +8 -3
  143. data/assets/js/page-feedback.js +125 -192
  144. data/assets/js/search-modal.js +26 -0
  145. data/assets/js/setup-wizard.js +2119 -363
  146. data/assets/js/site-builder.js +1834 -0
  147. data/assets/js/ui-enhancements.js +11 -3
  148. data/scripts/README.md +15 -0
  149. data/scripts/ai/README.md +38 -0
  150. data/scripts/ai/api_call.rb +124 -0
  151. data/scripts/ai/usage.rb +314 -0
  152. data/scripts/ai/usage_report.rb +225 -0
  153. data/scripts/ci/test_visual_evidence_autogen.py +403 -0
  154. data/scripts/ci/visual_evidence_autogen.py +1070 -0
  155. data/scripts/content-review.rb +20 -1
  156. data/scripts/features/install-preview-generator +1 -1
  157. data/scripts/lib/README.md +12 -0
  158. data/scripts/lib/preview_generator.py +2 -2
  159. data/scripts/test/integration/mermaid +22 -8
  160. data/scripts/test/lib/run_tests.sh +1 -0
  161. data/scripts/test/lib/test_visual_evidence_autogen.sh +24 -0
  162. data/scripts/translate.rb +23 -1
  163. metadata +21 -2
data/assets/js/ai-chat.js CHANGED
@@ -28,8 +28,10 @@
28
28
  * the tool_result back, and continues the loop.
29
29
  *
30
30
  * Security:
31
- * - Proxy mode (recommended) keeps the Anthropic key and GitHub token
32
- * server-side; the browser only ever talks to the same-origin proxy.
31
+ * - Proxy mode (recommended) keeps the provider key and GitHub token
32
+ * server-side; the browser only ever talks to the same-origin proxy,
33
+ * which answers with Claude or Grok (the reply is always Anthropic
34
+ * Messages SSE — the proxy translates for xAI).
33
35
  * - Direct mode sends x-api-key from the page and requires the
34
36
  * anthropic-dangerous-direct-browser-access header — local dev only.
35
37
  * - GitHub "url" mode never touches a token: it opens pre-filled
@@ -586,14 +588,53 @@
586
588
 
587
589
  // URL mode: open a pre-filled github.com form — the user submits it
588
590
  // with their own account, so no token is ever needed in the browser.
589
- var params = new URLSearchParams();
590
- params.set('title', String(inputData.title).slice(0, 256));
591
+ //
592
+ // The URL is composed by the shared fleet builder (FleetFeedbackCore, in
593
+ // assets/js/fleet-feedback.js) when the page-feedback widget is enabled,
594
+ // which is what removed the THIRD hand-rolled issue-URL builder in this
595
+ // theme. Two things come with it: the same `<!-- fleet-feedback v1 -->`
596
+ // marker the issue pipeline reads, and a real over-budget path — this used
597
+ // to `.slice(0, 6000)` the body, silently dropping whatever Claude had
598
+ // written past that point, with nobody the wiser.
591
599
  var body = String(inputData.body);
592
600
  if (meta.page_url && body.indexOf(meta.page_url) === -1) {
593
601
  body += '\n\n---\nPage: ' + meta.page_url;
594
602
  }
595
- params.set('body', body.slice(0, 6000));
596
603
  var labels = mergedLabels(inputData.labels);
604
+ var title = String(inputData.title).slice(0, 256);
605
+ var FF = window.FleetFeedbackCore;
606
+
607
+ if (FF) {
608
+ var issue = {
609
+ title: title,
610
+ // The chat writes prose, not a capture, so it fills the description
611
+ // slot whole; only the footer is contributed by the contract.
612
+ sections: {
613
+ description: body,
614
+ footer: '---\n_Filed from ' + (meta.page_url || location.href) +
615
+ ' via the AI chat assistant (fleet-feedback v' + FF.VERSION + ')._\n' +
616
+ '<!-- fleet-feedback v1 type=chat -->'
617
+ },
618
+ labels: labels,
619
+ assignees: []
620
+ };
621
+ var built = FF.buildUrl(issue, { repo: CONFIG.github.repository });
622
+ var win = window.open(built.url, '_blank', 'noopener');
623
+ if (!win) {
624
+ return toolResult(block.id, 'The browser blocked the pop-up, so no issue form opened. Ask the user to allow pop-ups for this site and try again.', true);
625
+ }
626
+ if (built.overBudget && navigator.clipboard && navigator.clipboard.writeText) {
627
+ navigator.clipboard.writeText(built.fullBody).catch(function () {});
628
+ return toolResult(block.id, 'A pre-filled GitHub issue form was opened in a new browser tab. The report was too long for the URL, so the full text was copied to the user clipboard — tell them to paste it over the pre-filled body before submitting.');
629
+ }
630
+ return toolResult(block.id, 'A pre-filled GitHub issue form was opened in a new browser tab. The user reviews and submits it there (a GitHub account is required).');
631
+ }
632
+
633
+ // No page-feedback widget on this site, so the shared builder is not
634
+ // loaded. Compose the URL directly; the body is capped rather than trimmed.
635
+ var params = new URLSearchParams();
636
+ params.set('title', title);
637
+ params.set('body', body.slice(0, 6000));
597
638
  if (labels.length) params.set('labels', labels.join(','));
598
639
  window.open('https://github.com/' + CONFIG.github.repository + '/issues/new?' + params.toString(), '_blank', 'noopener');
599
640
  return toolResult(block.id, 'A pre-filled GitHub issue form was opened in a new browser tab. The user reviews and submits it there (a GitHub account is required).');
@@ -735,6 +776,7 @@
735
776
  var accumulated = '';
736
777
  var payload = {
737
778
  model: CONFIG.model,
779
+ provider: CONFIG.provider || 'anthropic', // honoured by the proxy only when that provider has a key
738
780
  max_tokens: CONFIG.maxTokens,
739
781
  system: buildSystemPrompt(meta),
740
782
  messages: history.slice(),
@@ -34,21 +34,61 @@
34
34
  // The measured height is also published as --zer0-header-height so
35
35
  // stylesheets (theme or consumer) can align overlays with the real
36
36
  // header instead of hard-coding its pixel height.
37
- function updateBodyPadding() {
38
- const navbarHeight = navbar.offsetHeight;
39
- document.body.style.paddingTop = navbarHeight + 'px';
40
- document.documentElement.style.setProperty('--zer0-header-height', navbarHeight + 'px');
37
+ //
38
+ // Layout is never read synchronously here. Reading navbar.offsetHeight
39
+ // at DOMContentLoaded (and again on every resize) forced a style +
40
+ // layout pass while stylesheets were still settling (Lighthouse
41
+ // "forced reflow", ~233ms per read on a throttled phone). Instead:
42
+ // - ResizeObserver delivers the navbar's border-box height after the
43
+ // browser's own layout pass, before paint, so the padding still
44
+ // lands in the first frame;
45
+ // - without ResizeObserver, the read happens in a requestAnimationFrame
46
+ // callback, and the write in the frame after it, so a read never
47
+ // follows a write in the same pass;
48
+ // - the last height is cached and nothing is written when it is
49
+ // unchanged.
50
+ let lastNavbarHeight = -1;
51
+
52
+ function applyNavbarHeight(height) {
53
+ height = Math.round(height);
54
+ if (height === lastNavbarHeight) return;
55
+ lastNavbarHeight = height;
56
+ document.body.style.paddingTop = height + 'px';
57
+ document.documentElement.style.setProperty('--zer0-header-height', height + 'px');
41
58
  }
42
59
 
43
- // Initial padding setup
44
- updateBodyPadding();
45
-
46
- // Update padding on window resize with debounce
47
- let resizeTimeout;
48
- window.addEventListener('resize', function() {
49
- clearTimeout(resizeTimeout);
50
- resizeTimeout = setTimeout(updateBodyPadding, 150);
51
- }, { passive: true });
60
+ if (typeof window.ResizeObserver === 'function') {
61
+ const navbarObserver = new ResizeObserver(function(entries) {
62
+ const entry = entries[entries.length - 1];
63
+ const box = entry.borderBoxSize && (entry.borderBoxSize[0] || entry.borderBoxSize);
64
+ // Layout is already clean inside a ResizeObserver callback, so
65
+ // the offsetHeight fallback (old engines without borderBoxSize)
66
+ // does not force a reflow.
67
+ applyNavbarHeight(box && box.blockSize ? box.blockSize : entry.target.offsetHeight);
68
+ });
69
+ navbarObserver.observe(navbar);
70
+ } else {
71
+ let measureQueued = false;
72
+ const measureNavbar = function() {
73
+ if (measureQueued) return;
74
+ measureQueued = true;
75
+ window.requestAnimationFrame(function() {
76
+ const height = navbar.offsetHeight; // read
77
+ window.requestAnimationFrame(function() {
78
+ measureQueued = false;
79
+ applyNavbarHeight(height); // write, next frame
80
+ });
81
+ });
82
+ };
83
+ measureNavbar();
84
+
85
+ // Update padding on window resize with debounce
86
+ let resizeTimeout;
87
+ window.addEventListener('resize', function() {
88
+ clearTimeout(resizeTimeout);
89
+ resizeTimeout = setTimeout(measureNavbar, 150);
90
+ }, { passive: true });
91
+ }
52
92
 
53
93
  // Enhanced scroll handler with better logic
54
94
  function handleScroll() {
@@ -1,4 +1,53 @@
1
1
  // Feature: ZER0-030
2
+ //
3
+ // CONSUMER CONTRACT — code-block extension points (#412).
4
+ // ------------------------------------------------------
5
+ // `.code-block-header` is built HERE, at runtime, not emitted at build time, so
6
+ // a consumer adding a second action to code blocks has nothing in the markup to
7
+ // hook. Before this contract existed the only route was a `MutationObserver`
8
+ // over `#main-content` plus a guess at the wrapper's shape — and wrapper
9
+ // nesting is exactly what a theme refactor changes, so the guess was one
10
+ // release away from breaking. it-journey#634 got it wrong and silently rendered
11
+ // two buttons on every block (26 across 13) until it added a claim marker.
12
+ //
13
+ // Two ways in, both stable:
14
+ //
15
+ // document.addEventListener('zer0:code-block-ready', function (e) {
16
+ // e.detail; // { wrapper, header, pre, code, lang }
17
+ // });
18
+ //
19
+ // window.zer0OnCodeBlock(function (detail) { … }); // same shape, REPLAYED
20
+ //
21
+ // The event bubbles from the wrapper, fires exactly once per code block, and is
22
+ // dispatched after the block is fully decorated — line numbers, a11y attributes
23
+ // and the copy button are all in place before a consumer sees it. `header` is
24
+ // null for a standalone `<pre>` outside a Rouge wrapper, which has no header
25
+ // row; `lang` is null when the block declares no language.
26
+ //
27
+ // Because the sweep runs on `DOMContentLoaded`, a listener registered later
28
+ // would miss every block — so `window.__zer0CodeBlocks` holds the detail of
29
+ // every block already processed, and `zer0OnCodeBlock` replays that array
30
+ // before subscribing. Late registration is the NORMAL case for a deferred
31
+ // consumer script, not an edge case.
32
+ //
33
+ // Docs: docs/development/EXTENSION-POINTS.md
34
+ window.__zer0CodeBlocks = window.__zer0CodeBlocks || [];
35
+
36
+ window.zer0OnCodeBlock = function (listener) {
37
+ if (typeof listener !== 'function') return function () {};
38
+ window.__zer0CodeBlocks.forEach(function (detail) {
39
+ listener(detail);
40
+ });
41
+ var handler = function (event) {
42
+ listener(event.detail);
43
+ };
44
+ document.addEventListener('zer0:code-block-ready', handler);
45
+ // Returned so a consumer can stop listening; harmless to ignore.
46
+ return function () {
47
+ document.removeEventListener('zer0:code-block-ready', handler);
48
+ };
49
+ };
50
+
2
51
  document.addEventListener('DOMContentLoaded', function () {
3
52
  var LANG_LABELS = {
4
53
  shell: 'bash',
@@ -157,12 +206,18 @@ document.addEventListener('DOMContentLoaded', function () {
157
206
  });
158
207
  });
159
208
 
209
+ var wrapper;
210
+ var header = null;
160
211
  if (rougeWrapper) {
161
- var header = ensureHeader(rougeWrapper, lang);
212
+ wrapper = rougeWrapper;
213
+ header = ensureHeader(rougeWrapper, lang);
162
214
  header.appendChild(button);
163
215
  rougeWrapper.classList.toggle('code-block--single-line', isSingleLine);
164
216
  rougeWrapper.closest('.highlighter-rouge').classList.add('has-code-header');
165
217
  } else {
218
+ // A standalone <pre> is its own wrapper and has no header row: the copy
219
+ // button is positioned against the <pre> itself.
220
+ wrapper = preElement;
166
221
  if (getComputedStyle(preElement).position === 'static') {
167
222
  preElement.style.position = 'relative';
168
223
  }
@@ -171,5 +226,21 @@ document.addEventListener('DOMContentLoaded', function () {
171
226
  }
172
227
 
173
228
  preElement.classList.add('has-copy-button');
229
+
230
+ // Published LAST, so a consumer never sees a half-decorated block. Exactly
231
+ // once per block: `preElements` is a Set, and the `.copy` guard above makes
232
+ // a second sweep over the same block a no-op. See the contract at the top.
233
+ var detail = {
234
+ wrapper: wrapper,
235
+ header: header,
236
+ pre: preElement,
237
+ code: codeElement,
238
+ lang: lang
239
+ };
240
+ window.__zer0CodeBlocks.push(detail);
241
+ wrapper.dispatchEvent(new CustomEvent('zer0:code-block-ready', {
242
+ bubbles: true,
243
+ detail: detail
244
+ }));
174
245
  });
175
246
  });
@@ -0,0 +1,48 @@
1
+ // =============================================================================
2
+ // extension-points-demo.js — a live consumer of the theme's extension points
3
+ // =============================================================================
4
+ //
5
+ // Loaded ONLY by /docs/customization/extension-points/, through that page's
6
+ // `scripts:` frontmatter — which makes this file a demonstration of two
7
+ // contracts at once: the per-page script hook that loads it, and the
8
+ // `zer0:code-block-ready` event it subscribes to.
9
+ //
10
+ // It is deliberately the whole pattern in a dozen lines. The point of #412 is
11
+ // that a consumer adding an action to a code block should not need a
12
+ // MutationObserver, a timeout fallback, or a guess at how deeply the theme
13
+ // nests its wrappers — all three of which it-journey#634 had to carry, and the
14
+ // wrapper guess is what silently produced two buttons per block.
15
+ //
16
+ // Copy this shape. `zer0OnCodeBlock` replays the blocks that were decorated
17
+ // before this script ran, then subscribes for any that follow, so it does not
18
+ // matter whether you load early or late.
19
+ // =============================================================================
20
+ (function () {
21
+ 'use strict';
22
+
23
+ if (typeof window.zer0OnCodeBlock !== 'function') return;
24
+
25
+ window.zer0OnCodeBlock(function (detail) {
26
+ // A standalone <pre> outside a Rouge wrapper has no header row to hang an
27
+ // action on. `header` is null there, and that is the documented signal.
28
+ if (!detail.header) return;
29
+ if (detail.header.querySelector('.zer0-demo-action')) return;
30
+
31
+ var lines = (detail.code.textContent || '').replace(/\n$/, '').split('\n').length;
32
+
33
+ var button = document.createElement('button');
34
+ button.type = 'button';
35
+ button.className = 'zer0-demo-action';
36
+ button.textContent = lines + (lines === 1 ? ' line' : ' lines');
37
+ button.setAttribute(
38
+ 'aria-label',
39
+ 'This ' + (detail.lang || 'code') + ' block has ' + lines + ' lines'
40
+ );
41
+ button.addEventListener('click', function () {
42
+ detail.pre.focus();
43
+ });
44
+
45
+ // Before the copy button, so Copy keeps its place at the end of the row.
46
+ detail.header.insertBefore(button, detail.header.querySelector('.copy'));
47
+ });
48
+ })();
@@ -0,0 +1,124 @@
1
+ /*!
2
+ * fleet-feedback capture v0.2.0 — the early console/error ring buffer.
3
+ * Source: bamr87/bamr87 templates/feedback/ (spec: specs/FEEDBACK.md, UPS-FB-04).
4
+ *
5
+ * Load this FIRST — before any other script, as high in <head> as you can get
6
+ * it — so the errors that happen during page load are in the buffer by the time
7
+ * a reader opens the feedback dialog. fleet-feedback.js installs the same
8
+ * buffer if this file is absent, but by then the interesting failures have
9
+ * already happened silently.
10
+ *
11
+ * <script src="/assets/js/fleet-feedback-capture.js"></script> <!-- Jekyll/Django -->
12
+ * <Script src="/fleet-feedback-capture.js" strategy="beforeInteractive" /> <!-- Next.js -->
13
+ *
14
+ * Small enough to inline verbatim into a <script> tag when a extra request
15
+ * costs more than the bytes (the Jekyll adapter does exactly that).
16
+ *
17
+ * Contract — window.__fleetFeedback:
18
+ * logs [{ t: ISO-8601, level: 'warn'|'error'|'net', msg: string }]
19
+ * limit ring size (default 40)
20
+ * installed true once the hooks are in place; a second load is a no-op
21
+ * version kit version that installed it
22
+ * breadcrumb(level, message) push a structured entry (fetch wrappers use this)
23
+ * snapshot() a copy of the buffer, newest last
24
+ * clear() empty it
25
+ *
26
+ * Privacy: in-memory only. Never persisted, never transmitted, never read until
27
+ * the reader opens the dialog, previews the lines, and submits. Every entry is
28
+ * redacted (bearer tokens, API keys, JWTs, email addresses) and truncated.
29
+ */
30
+ /* fleet-feedback:capture:begin — byte-identical in fleet-feedback.js (tests/contract.test.mjs asserts it) */
31
+ (function (root) {
32
+ 'use strict';
33
+
34
+ var VERSION = '0.2.0';
35
+ var LIMIT = 40;
36
+ var MAX_ENTRY = 600;
37
+
38
+ var g = (root.__fleetFeedback = root.__fleetFeedback || {
39
+ logs: [],
40
+ limit: LIMIT,
41
+ installed: false,
42
+ version: VERSION
43
+ });
44
+ if (g.installed) { return; }
45
+
46
+ // Secrets end up in console output far more often than anyone expects — an
47
+ // Authorization header logged by a fetch wrapper, a signed URL in a 403, a
48
+ // user's address in a validation message. Redact BEFORE the line enters the
49
+ // buffer, so it can never be previewed, copied, or filed.
50
+ var SCRUB = [
51
+ // The credential can carry a scheme word of its own ("Authorization: Bearer
52
+ // <token>"), and a rule that stops at the scheme leaves the secret in the
53
+ // buffer — which is exactly what the first version of this did.
54
+ [/\b(authorization|proxy-authorization|bearer|token|api[-_]?key|access[-_]?token|refresh[-_]?token|client[-_]?secret|secret|password|passwd)(\s*[:=]\s*|\s+)((?:bearer|basic|digest|token)\s+)?([^\s,;"'&]{6,})/gi, '$1$2$3***'],
55
+ [/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]+/g, '***jwt***'],
56
+ [/\bgh[pousr]_[A-Za-z0-9]{16,}/g, '***github-token***'],
57
+ [/\bsk-[A-Za-z0-9_-]{16,}/g, '***api-key***'],
58
+ [/\b[\w.+-]+@[\w-]+\.[\w.-]{2,}\b/g, '***@***']
59
+ ];
60
+
61
+ function redact(text) {
62
+ var out = String(text == null ? '' : text);
63
+ for (var i = 0; i < SCRUB.length; i++) {
64
+ out = out.replace(SCRUB[i][0], SCRUB[i][1]);
65
+ }
66
+ return out;
67
+ }
68
+
69
+ function stringify(value) {
70
+ if (typeof value === 'string') { return value; }
71
+ if (value instanceof Error) {
72
+ return (value.name || 'Error') + ': ' + (value.message || '') +
73
+ (value.stack ? '\n' + String(value.stack).split('\n').slice(1, 4).join('\n') : '');
74
+ }
75
+ try { return JSON.stringify(value); } catch (e) { return String(value); }
76
+ }
77
+
78
+ function push(level, parts) {
79
+ try {
80
+ var line = redact(parts.map(stringify).join(' '));
81
+ if (line.length > MAX_ENTRY) { line = line.slice(0, MAX_ENTRY) + '…'; }
82
+ g.logs.push({ t: new Date().toISOString(), level: level, msg: line });
83
+ while (g.logs.length > g.limit) { g.logs.shift(); }
84
+ } catch (e) { /* capture must never break the page it is watching */ }
85
+ }
86
+
87
+ // Only warn/error are hooked. console.log is noise at this budget — 40 lines
88
+ // of debug chatter push the actual failure out of a ring this size.
89
+ if (root.console) {
90
+ ['warn', 'error'].forEach(function (level) {
91
+ var original = root.console[level];
92
+ root.console[level] = function () {
93
+ push(level, Array.prototype.slice.call(arguments));
94
+ if (original) { return original.apply(root.console, arguments); }
95
+ };
96
+ });
97
+ }
98
+
99
+ if (root.addEventListener) {
100
+ root.addEventListener('error', function (e) {
101
+ if (!e) { return; }
102
+ var where = e.filename ? ' (' + e.filename + ':' + (e.lineno || 0) + ':' + (e.colno || 0) + ')' : '';
103
+ push('error', [(e.message || 'Uncaught error') + where]);
104
+ });
105
+ root.addEventListener('unhandledrejection', function (e) {
106
+ var reason = e && e.reason;
107
+ push('error', ['Unhandled promise rejection:', reason && reason.message ? reason.message : reason]);
108
+ });
109
+ }
110
+
111
+ g.version = VERSION;
112
+ g.installed = true;
113
+ g.redact = redact;
114
+ /**
115
+ * Push a structured entry. An app's fetch wrapper calls this on a failed
116
+ * request so the report carries the API call that broke, not just the
117
+ * exception it surfaced as:
118
+ * FF.breadcrumb('net', 'GET /api/analysis → 502 (1243ms) req=abc123')
119
+ */
120
+ g.breadcrumb = function (level, message) { push(level || 'net', [message]); };
121
+ g.snapshot = function () { return g.logs.slice(); };
122
+ g.clear = function () { g.logs.length = 0; };
123
+ })(typeof window !== 'undefined' ? window : globalThis);
124
+ /* fleet-feedback:capture:end */