Compare commits
13
Commits
6b07b83583
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fccc0115c4 | ||
|
|
189260bbb3 | ||
|
|
8e7070cbaf | ||
|
|
a8d7db562d | ||
|
|
e220f46d35 | ||
|
|
93460ece19 | ||
|
|
7b67897612 | ||
|
|
c5f79f1fd2 | ||
|
|
41abcaa7a4 | ||
|
|
966f3b23fa | ||
|
|
45b30918a0 | ||
|
|
7eeec7e781 | ||
|
|
527131a462 |
@@ -1,11 +1,11 @@
|
||||
# Graph Report - 2e709500-79ab-403f-9ca0-3f027ac9f36e (2026-06-19)
|
||||
# Graph Report - 2e709500-79ab-403f-9ca0-3f027ac9f36e (2026-06-24)
|
||||
|
||||
## Corpus Check
|
||||
- 61 files · ~611,807 words
|
||||
- 64 files · ~615,668 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
- 596 nodes · 620 edges · 114 communities (87 shown, 27 thin omitted)
|
||||
- 619 nodes · 640 edges · 117 communities (90 shown, 27 thin omitted)
|
||||
- Extraction: 99% EXTRACTED · 1% INFERRED · 0% AMBIGUOUS · INFERRED: 4 edges (avg confidence: 0.8)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
@@ -50,9 +50,8 @@
|
||||
- [[_COMMUNITY_Community 34|Community 34]]
|
||||
- [[_COMMUNITY_Community 35|Community 35]]
|
||||
- [[_COMMUNITY_Community 36|Community 36]]
|
||||
- [[_COMMUNITY_Community 41|Community 41]]
|
||||
- [[_COMMUNITY_Community 42|Community 42]]
|
||||
- [[_COMMUNITY_Community 43|Community 43]]
|
||||
- [[_COMMUNITY_Community 37|Community 37]]
|
||||
- [[_COMMUNITY_Community 38|Community 38]]
|
||||
- [[_COMMUNITY_Community 44|Community 44]]
|
||||
- [[_COMMUNITY_Community 45|Community 45]]
|
||||
- [[_COMMUNITY_Community 46|Community 46]]
|
||||
@@ -71,6 +70,9 @@
|
||||
- [[_COMMUNITY_Community 59|Community 59]]
|
||||
- [[_COMMUNITY_Community 60|Community 60]]
|
||||
- [[_COMMUNITY_Community 61|Community 61]]
|
||||
- [[_COMMUNITY_Community 62|Community 62]]
|
||||
- [[_COMMUNITY_Community 63|Community 63]]
|
||||
- [[_COMMUNITY_Community 64|Community 64]]
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `Performance Testing Scripts` - 12 edges
|
||||
@@ -94,7 +96,7 @@
|
||||
- `POST()` --calls--> `logApiRequest()` [INFERRED]
|
||||
src/pages/api/newsletter.ts → src/utils/logger.ts
|
||||
|
||||
## Communities (114 total, 27 thin omitted)
|
||||
## Communities (117 total, 27 thin omitted)
|
||||
|
||||
### Community 0 - "Community 0"
|
||||
Cohesion: 0.05
|
||||
@@ -144,49 +146,49 @@ Nodes (11): Code Example, code:astro (---), code:javascript (// Traditional SPA:
|
||||
Cohesion: 0.18
|
||||
Nodes (10): code:hcl (# Define AWS EC2 instance), code:dockerfile (# Multi-stage build for a Go application), code:yaml (# AWS Cost allocation tags), Common Pitfalls, Conclusion, Containerization as a Migration Strategy, Cost Optimization Strategies, Infrastructure as Code (+2 more)
|
||||
|
||||
### Community 18 - "Community 18"
|
||||
Cohesion: 0.43
|
||||
Nodes (6): trackCaseStudyView(), trackEvent(), trackExternalLink(), trackFormSubmit(), trackNewsletterSignup(), trackPortfolioFilter()
|
||||
### Community 16 - "Community 16"
|
||||
Cohesion: 0.2
|
||||
Nodes (9): Catalogue Listing vs Bidding: How You Actually Win, Common Reasons Software Vendors Get Rejected or Delayed, Document Checklist, Fees, Caution Money, and Timelines, Frequently Asked Questions, MSME and Startup Benefits Worth Claiming, Step-by-Step Registration Process, Turning GeM Access Into Government Contracts (+1 more)
|
||||
|
||||
### Community 19 - "Community 19"
|
||||
Cohesion: 0.36
|
||||
Nodes (7): extract_scores(), get_issues(), main(), Run Lighthouse audit on a URL, Extract key scores from Lighthouse data, Extract issues from Lighthouse audit, run_lighthouse()
|
||||
### Community 18 - "Community 18"
|
||||
Cohesion: 0.22
|
||||
Nodes (8): 2026 Cost Ranges by Project Tier, A Simple Way to Estimate Your Own Cost, Build vs Buy: When Custom Actually Pays Off, Frequently Asked Questions, Getting an Accurate Number for Your Project, Hidden Costs Buyers Forget to Budget, The Biggest Cost Driver: The Team, What "Custom ERP" Actually Means for Pricing
|
||||
|
||||
### Community 20 - "Community 20"
|
||||
Cohesion: 0.46
|
||||
Nodes (7): captureScreenshot(), ensureDir(), generateIndex(), main(), screenshotPath(), setTheme(), waitForPageReady()
|
||||
Cohesion: 0.43
|
||||
Nodes (6): trackCaseStudyView(), trackEvent(), trackExternalLink(), trackFormSubmit(), trackNewsletterSignup(), trackPortfolioFilter()
|
||||
|
||||
### Community 21 - "Community 21"
|
||||
Cohesion: 0.36
|
||||
Nodes (4): deleteFromDB(), getAllFromDB(), openDB(), processSyncQueue()
|
||||
|
||||
### Community 22 - "Community 22"
|
||||
Cohesion: 0.43
|
||||
Nodes (6): trackCaseStudyView(), trackEvent(), trackExternalLink(), trackFormSubmit(), trackNewsletterSignup(), trackPortfolioFilter()
|
||||
|
||||
### Community 23 - "Community 23"
|
||||
Cohesion: 0.46
|
||||
Nodes (7): checkRateLimit(), corsHeaders(), escapeHtml(), OPTIONS(), POST(), sendContactEmail(), validateContactForm()
|
||||
|
||||
### Community 24 - "Community 24"
|
||||
Cohesion: 0.36
|
||||
Nodes (7): extract_scores(), get_issues(), main(), Run Lighthouse audit on a URL, Extract key scores from Lighthouse data, Extract issues from Lighthouse audit, run_lighthouse()
|
||||
|
||||
### Community 25 - "Community 25"
|
||||
### Community 22 - "Community 22"
|
||||
Cohesion: 0.46
|
||||
Nodes (7): captureScreenshot(), ensureDir(), generateIndex(), main(), screenshotPath(), setTheme(), waitForPageReady()
|
||||
|
||||
### Community 26 - "Community 26"
|
||||
### Community 23 - "Community 23"
|
||||
Cohesion: 0.36
|
||||
Nodes (4): deleteFromDB(), getAllFromDB(), openDB(), processSyncQueue()
|
||||
|
||||
### Community 24 - "Community 24"
|
||||
Cohesion: 0.46
|
||||
Nodes (7): checkRateLimit(), corsHeaders(), escapeHtml(), OPTIONS(), POST(), sendContactEmail(), validateContactForm()
|
||||
|
||||
### Community 25 - "Community 25"
|
||||
Cohesion: 0.43
|
||||
Nodes (6): trackCaseStudyView(), trackEvent(), trackExternalLink(), trackFormSubmit(), trackNewsletterSignup(), trackPortfolioFilter()
|
||||
|
||||
### Community 26 - "Community 26"
|
||||
Cohesion: 0.36
|
||||
Nodes (7): extract_scores(), get_issues(), main(), Run Lighthouse audit on a URL, Extract key scores from Lighthouse data, Extract issues from Lighthouse audit, run_lighthouse()
|
||||
|
||||
### Community 27 - "Community 27"
|
||||
Cohesion: 0.48
|
||||
Nodes (5): initAllAnimations(), initButtonRipple(), initCounters(), initProgressBars(), initScrollReveal()
|
||||
Cohesion: 0.46
|
||||
Nodes (7): captureScreenshot(), ensureDir(), generateIndex(), main(), screenshotPath(), setTheme(), waitForPageReady()
|
||||
|
||||
### Community 28 - "Community 28"
|
||||
Cohesion: 0.48
|
||||
Nodes (5): extractJsonLd(), main(), validateOpenGraph(), validatePage(), validateTwitterCard()
|
||||
Cohesion: 0.36
|
||||
Nodes (4): deleteFromDB(), getAllFromDB(), openDB(), processSyncQueue()
|
||||
|
||||
### Community 29 - "Community 29"
|
||||
Cohesion: 0.48
|
||||
@@ -197,23 +199,31 @@ Cohesion: 0.48
|
||||
Nodes (5): extractJsonLd(), main(), validateOpenGraph(), validatePage(), validateTwitterCard()
|
||||
|
||||
### Community 31 - "Community 31"
|
||||
Cohesion: 0.47
|
||||
Nodes (5): main(), measure_response_time(), print_stats(), Measure response time for a URL over multiple iterations, Print statistics for response times
|
||||
Cohesion: 0.48
|
||||
Nodes (5): initAllAnimations(), initButtonRipple(), initCounters(), initProgressBars(), initScrollReveal()
|
||||
|
||||
### Community 32 - "Community 32"
|
||||
Cohesion: 0.48
|
||||
Nodes (5): extractJsonLd(), main(), validateOpenGraph(), validatePage(), validateTwitterCard()
|
||||
|
||||
### Community 33 - "Community 33"
|
||||
Cohesion: 0.47
|
||||
Nodes (5): main(), measure_response_time(), print_stats(), Measure response time for a URL over multiple iterations, Print statistics for response times
|
||||
|
||||
### Community 35 - "Community 35"
|
||||
Cohesion: 0.47
|
||||
Nodes (5): main(), measure_response_time(), print_stats(), Measure response time for a URL over multiple iterations, Print statistics for response times
|
||||
|
||||
### Community 37 - "Community 37"
|
||||
Cohesion: 0.6
|
||||
Nodes (3): formatUptime(), GET(), percentile()
|
||||
|
||||
### Community 36 - "Community 36"
|
||||
### Community 38 - "Community 38"
|
||||
Cohesion: 0.6
|
||||
Nodes (3): formatUptime(), GET(), percentile()
|
||||
|
||||
## Knowledge Gaps
|
||||
- **140 isolated node(s):** `Run Lighthouse audit on a URL`, `Extract key scores from Lighthouse data`, `Extract issues from Lighthouse audit`, `Measure response time for a URL over multiple iterations`, `Print statistics for response times` (+135 more)
|
||||
- **156 isolated node(s):** `Run Lighthouse audit on a URL`, `Extract key scores from Lighthouse data`, `Extract issues from Lighthouse audit`, `Measure response time for a URL over multiple iterations`, `Print statistics for response times` (+151 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **27 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
|
||||
@@ -223,7 +233,7 @@ _Questions this graph is uniquely positioned to answer:_
|
||||
- **Are the 2 inferred relationships involving `captureException()` (e.g. with `sendContactEmail()` and `subscribeToNewsletter()`) actually correct?**
|
||||
_`captureException()` has 2 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **What connects `Run Lighthouse audit on a URL`, `Extract key scores from Lighthouse data`, `Extract issues from Lighthouse audit` to the rest of the system?**
|
||||
_140 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
_156 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
- **Should `Community 0` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.05 - nodes in this community are weakly interconnected._
|
||||
- **Should `Community 1` be split into smaller, more focused modules?**
|
||||
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "label": "middleware.ts", "file_type": "code", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L1"}, {"id": "src_middleware_validatecsrforigin", "label": "validateCsrfOrigin()", "file_type": "code", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L19"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "astro_middleware", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L1", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_utils_logger_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L2", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "utils_logger_logger", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L2", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_utils_sentry_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L3", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "utils_sentry_captureexception", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L3", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "src_middleware_validatecsrforigin", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L19", "weight": 1.0}], "raw_calls": [{"caller_nid": "src_middleware_validatecsrforigin", "callee": "includes", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L24"}, {"caller_nid": "src_middleware_validatecsrforigin", "callee": "startsWith", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L25"}, {"caller_nid": "src_middleware_validatecsrforigin", "callee": "get", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L36"}, {"caller_nid": "src_middleware_validatecsrforigin", "callee": "get", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L37"}, {"caller_nid": "src_middleware_validatecsrforigin", "callee": "startsWith", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L52"}, {"caller_nid": "src_middleware_validatecsrforigin", "callee": "startsWith", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L52"}]}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_pages_sitemap_xml_ts", "label": "sitemap.xml.ts", "file_type": "code", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L1"}, {"id": "pages_sitemap_xml_get", "label": "GET()", "file_type": "code", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L13"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_pages_sitemap_xml_ts", "target": "astro", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L1", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_pages_sitemap_xml_ts", "target": "astro_content", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L2", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_pages_sitemap_xml_ts", "target": "pages_sitemap_xml_get", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L13", "weight": 1.0}], "raw_calls": [{"caller_nid": "pages_sitemap_xml_get", "callee": "getCollection", "is_member_call": false, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L15"}, {"caller_nid": "pages_sitemap_xml_get", "callee": "getCollection", "is_member_call": false, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L20"}, {"caller_nid": "pages_sitemap_xml_get", "callee": "map", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L43"}, {"caller_nid": "pages_sitemap_xml_get", "callee": "map", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L52"}, {"caller_nid": "pages_sitemap_xml_get", "callee": "join", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L69"}, {"caller_nid": "pages_sitemap_xml_get", "callee": "map", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L69"}]}
|
||||
File diff suppressed because one or more lines are too long
+697
-290
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,166 @@
|
||||
/*!
|
||||
* WRNexus Web Push — service worker.
|
||||
*
|
||||
* ┌─────────────────────────────────────────────────────────────────────────┐
|
||||
* │ HOST THIS FILE ON YOUR OWN ORIGIN. │
|
||||
* │ │
|
||||
* │ A service worker can only control the origin it is served from, so the │
|
||||
* │ browser will NOT let wrnexus.com register a worker for your site. Copy │
|
||||
* │ this file to the ROOT of your site and serve it at: │
|
||||
* │ │
|
||||
* │ https://your-domain.example/wrnexus-push-sw.js │
|
||||
* │ │
|
||||
* │ It must be served same-origin, over HTTPS, with │
|
||||
* │ `Content-Type: text/javascript`. To control the whole site from a │
|
||||
* │ non-root path, also send `Service-Worker-Allowed: /`. │
|
||||
* └─────────────────────────────────────────────────────────────────────────┘
|
||||
*
|
||||
* Core behaviour needs NO configuration — the click/delivery tracking URLs are
|
||||
* carried inside each push payload built by the WRNexus API. The optional
|
||||
* CONFIG block below is only used to silently re-subscribe after the browser
|
||||
* rotates a subscription (`pushsubscriptionchange`); leave it blank to skip
|
||||
* that.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-restricted-globals */
|
||||
|
||||
// ── OPTIONAL: only needed for automatic re-subscription on key rotation ──────
|
||||
var WRNEXUS_PUSH_CONFIG = {
|
||||
// Your site's publicKey (the same value as data-site-id in the snippet).
|
||||
siteKey: '',
|
||||
// The WRNexus API origin (e.g. 'https://api.wrnexus.com').
|
||||
apiBase: '',
|
||||
// Your site's VAPID public key (shown in Push → Settings).
|
||||
vapidPublicKey: '',
|
||||
};
|
||||
|
||||
self.addEventListener('install', function () {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', function (event) {
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
||||
// CORS-simple POST (text/plain, no custom headers, no credentials) so the
|
||||
// receipt reaches the WRNexus API cross-origin without a preflight.
|
||||
function postReceipt(url) {
|
||||
if (!url) return Promise.resolve();
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'text/plain;charset=UTF-8' },
|
||||
credentials: 'omit',
|
||||
keepalive: true,
|
||||
mode: 'cors',
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
function urlBase64ToUint8Array(base64String) {
|
||||
var padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
||||
var base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
||||
var raw = self.atob(base64);
|
||||
var out = new Uint8Array(raw.length);
|
||||
for (var i = 0; i < raw.length; ++i) out[i] = raw.charCodeAt(i);
|
||||
return out;
|
||||
}
|
||||
|
||||
function keyToB64(buf) {
|
||||
if (!buf) return '';
|
||||
var bytes = new Uint8Array(buf);
|
||||
var binary = '';
|
||||
for (var i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
||||
return self
|
||||
.btoa(binary)
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '');
|
||||
}
|
||||
|
||||
// ── Incoming notification ─────────────────────────────────────────────────
|
||||
self.addEventListener('push', function (event) {
|
||||
var data = {};
|
||||
try {
|
||||
data = event.data ? event.data.json() : {};
|
||||
} catch (_e) {
|
||||
try {
|
||||
data = { title: event.data && event.data.text() };
|
||||
} catch (_e2) {
|
||||
data = {};
|
||||
}
|
||||
}
|
||||
|
||||
var title = data.title || 'Notification';
|
||||
var options = {
|
||||
body: data.body || '',
|
||||
icon: data.icon || undefined,
|
||||
image: data.image || undefined,
|
||||
badge: data.badge || undefined,
|
||||
tag: data.tag || data.campaignId || undefined,
|
||||
requireInteraction: !!data.requireInteraction,
|
||||
data: {
|
||||
clickUrl: data.clickUrl || data.url || '/',
|
||||
url: data.url || '/',
|
||||
openUrl: data.openUrl || '',
|
||||
campaignId: data.campaignId || '',
|
||||
subscriptionId: data.subscriptionId || '',
|
||||
},
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
Promise.all([
|
||||
self.registration.showNotification(title, options),
|
||||
// Delivery receipt — records a `delivered` event for analytics.
|
||||
postReceipt(data.deliveredUrl),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
// ── Click → record + open the destination ─────────────────────────────────
|
||||
self.addEventListener('notificationclick', function (event) {
|
||||
var d = (event.notification && event.notification.data) || {};
|
||||
event.notification.close();
|
||||
|
||||
// `clickUrl` is a signed WRNexus redirect: hitting it records the `clicked`
|
||||
// event and 302s to the real destination, so tracking fires from the open.
|
||||
var target = d.clickUrl || d.url || '/';
|
||||
|
||||
event.waitUntil(
|
||||
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function (list) {
|
||||
// Reuse an existing tab on the destination when possible.
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var client = list[i];
|
||||
if (d.url && client.url === d.url && 'focus' in client) return client.focus();
|
||||
}
|
||||
if (self.clients.openWindow) return self.clients.openWindow(target);
|
||||
return undefined;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// ── Optional: re-subscribe after the browser rotates the subscription ──────
|
||||
self.addEventListener('pushsubscriptionchange', function (event) {
|
||||
var cfg = WRNEXUS_PUSH_CONFIG || {};
|
||||
if (!cfg.siteKey || !cfg.apiBase || !cfg.vapidPublicKey) return;
|
||||
event.waitUntil(
|
||||
self.registration.pushManager
|
||||
.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(cfg.vapidPublicKey) })
|
||||
.then(function (sub) {
|
||||
return fetch(cfg.apiBase.replace(/\/+$/, '') + '/api/push/subscriptions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'text/plain;charset=UTF-8' },
|
||||
credentials: 'omit',
|
||||
mode: 'cors',
|
||||
body: JSON.stringify({
|
||||
key: cfg.siteKey,
|
||||
endpoint: sub.endpoint,
|
||||
keys: {
|
||||
p256dh: keyToB64(sub.getKey('p256dh')),
|
||||
auth: keyToB64(sub.getKey('auth')),
|
||||
},
|
||||
metadata: { source: 'pushsubscriptionchange' },
|
||||
}),
|
||||
}).catch(function () {});
|
||||
})
|
||||
.catch(function () {})
|
||||
);
|
||||
});
|
||||
@@ -166,6 +166,23 @@ const currentYear = new Date().getFullYear();
|
||||
By subscribing, you agree to our
|
||||
<a href="/privacy" class="text-primary hover:underline">Privacy Policy</a>.
|
||||
</p>
|
||||
|
||||
<!-- Web push opt-in (WRNexus). User-gesture subscribe control.
|
||||
Hidden by default; the client script below reveals it only when the
|
||||
WRNexus SDK + browser push support are available, and sets the
|
||||
correct label based on the visitor's current subscription state. -->
|
||||
<button
|
||||
id="wrnexus-subscribe"
|
||||
type="button"
|
||||
hidden
|
||||
aria-label="Enable notifications"
|
||||
class="mt-6 w-full px-4 py-3 bg-secondary-800 border border-secondary-700 text-white font-medium rounded-lg hover:bg-secondary-700 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-secondary-900 transition-all duration-200 text-sm flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
>
|
||||
<svg class="w-4 h-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
||||
</svg>
|
||||
<span id="wrnexus-subscribe-label">Enable notifications</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,6 +210,106 @@ const currentYear = new Date().getFullYear();
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- WRNexus push subscribe: state-aware control wired via addEventListener
|
||||
(Astro strips inline on* handlers). The button reflects whether the
|
||||
visitor is already subscribed and lets them toggle the subscription. -->
|
||||
<script is:inline>
|
||||
(function () {
|
||||
function init() {
|
||||
var btn = document.getElementById('wrnexus-subscribe');
|
||||
var label = document.getElementById('wrnexus-subscribe-label');
|
||||
if (!btn || !label) return;
|
||||
|
||||
var push = window.WRNexusPush;
|
||||
var supported =
|
||||
push &&
|
||||
'Notification' in window &&
|
||||
'serviceWorker' in navigator &&
|
||||
'PushManager' in window;
|
||||
|
||||
// No SDK or no browser push support: keep the button hidden entirely.
|
||||
if (!supported) {
|
||||
btn.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Permission permanently blocked by the user: the SDK can't re-prompt,
|
||||
// so surface a clear, non-actionable disabled state instead of a button
|
||||
// that silently does nothing.
|
||||
if (window.Notification && Notification.permission === 'denied') {
|
||||
label.textContent = 'Notifications blocked';
|
||||
btn.setAttribute('aria-label', 'Notifications are blocked in your browser settings');
|
||||
btn.disabled = true;
|
||||
btn.hidden = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Render the button for a given subscription state.
|
||||
// Default UX: subscribed users get an actionable "Unsubscribe" toggle
|
||||
// rather than a dead "Already subscribed" label.
|
||||
// (Alternative: render a disabled "Already subscribed" indicator instead
|
||||
// of the Unsubscribe toggle if product prefers a non-actionable state.)
|
||||
function render(subscribed) {
|
||||
if (subscribed) {
|
||||
label.textContent = 'Unsubscribe';
|
||||
btn.setAttribute('aria-label', 'Unsubscribe from notifications');
|
||||
} else {
|
||||
label.textContent = 'Enable notifications';
|
||||
btn.setAttribute('aria-label', 'Enable notifications');
|
||||
}
|
||||
btn.dataset.subscribed = subscribed ? 'true' : 'false';
|
||||
btn.disabled = false;
|
||||
btn.hidden = false;
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
var subscribed = await push.isSubscribed();
|
||||
render(!!subscribed);
|
||||
} catch (err) {
|
||||
// On error, fall back to the unsubscribed label so the control stays usable.
|
||||
console.error('[wrnexus-push] isSubscribed() failed:', err);
|
||||
render(false);
|
||||
}
|
||||
}
|
||||
|
||||
btn.addEventListener('click', async function () {
|
||||
if (btn.disabled) return;
|
||||
|
||||
var wasSubscribed = btn.dataset.subscribed === 'true';
|
||||
var busyLabel = wasSubscribed ? 'Unsubscribing…' : 'Enabling…';
|
||||
|
||||
// In-progress state to prevent double-clicks while the promise is pending.
|
||||
btn.disabled = true;
|
||||
label.textContent = busyLabel;
|
||||
|
||||
try {
|
||||
if (wasSubscribed) {
|
||||
await push.unsubscribe();
|
||||
} else {
|
||||
// prompt() must run from this real user gesture.
|
||||
await push.prompt();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[wrnexus-push] toggle failed:', err);
|
||||
} finally {
|
||||
// Re-read the authoritative state and re-render the label.
|
||||
await refresh();
|
||||
}
|
||||
});
|
||||
|
||||
// Determine and render the initial state.
|
||||
refresh();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { trackNewsletterSignup } from '../utils/analytics';
|
||||
|
||||
|
||||
@@ -181,6 +181,17 @@ const fullTitle = title === 'Home' ? homeTitle : `${title} | ${brand}`;
|
||||
<!-- Analytics (GA4 or Plausible) -->
|
||||
<Analytics />
|
||||
|
||||
<!-- WRNexus Web Push SDK — loaded once globally so it runs on every page
|
||||
that should collect push subscribers. is:inline keeps Astro's bundler
|
||||
from rewriting the external src and preserves data-site-id / defer. -->
|
||||
<script is:inline src="https://api.wrnexus.com/push/sdk.js" data-site-id="pk_rP51XN8tZnud5HdX9UqidjHk" defer></script>
|
||||
|
||||
<!-- WRNexus Bot Widget — loaded once globally here so the chat widget
|
||||
renders on every route (pages + blog posts) via this shared layout.
|
||||
is:inline keeps Astro's bundler from rewriting the external src and
|
||||
preserves data-bot-id / async. -->
|
||||
<script is:inline src="https://api.wrnexus.com/bots/widget.js" data-bot-id="d3998f1b-51d7-4f7c-a57e-3fa1af4394a0" async></script>
|
||||
|
||||
<!-- Additional head content slot -->
|
||||
<slot name="head" />
|
||||
</head>
|
||||
|
||||
+6
-4
@@ -155,11 +155,13 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
||||
// - inline scripts/styles: Required for Astro framework
|
||||
// - https://www.googletagmanager.com & https://www.google-analytics.com: GA4
|
||||
// - https://plausible.io: Plausible Analytics
|
||||
// - https://api.wrnexus.com: WRNexus web-push SDK (sdk.js), bot widget
|
||||
// (bots/widget.js) + runtime config/iframe served from the same origin
|
||||
// Note: unsafe-eval is removed in production (only needed during Astro HMR dev mode)
|
||||
const isProduction = import.meta.env.PROD;
|
||||
const scriptSrc = isProduction
|
||||
? "script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com https://plausible.io"
|
||||
: "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://www.google-analytics.com https://plausible.io";
|
||||
? "script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com https://plausible.io https://api.wrnexus.com"
|
||||
: "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://www.google-analytics.com https://plausible.io https://api.wrnexus.com";
|
||||
|
||||
const csp = [
|
||||
"default-src 'self'",
|
||||
@@ -167,11 +169,11 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
||||
"img-src 'self' data: https://images.unsplash.com https://www.google-analytics.com https://www.googletagmanager.com https://maps.googleapis.com https://maps.gstatic.com",
|
||||
"font-src 'self' data: https://fonts.gstatic.com",
|
||||
"connect-src 'self' https://www.google-analytics.com https://analytics.google.com https://stats.g.doubleclick.net https://plausible.io",
|
||||
"connect-src 'self' https://www.google-analytics.com https://analytics.google.com https://stats.g.doubleclick.net https://plausible.io https://api.wrnexus.com",
|
||||
"worker-src 'self'",
|
||||
"manifest-src 'self'",
|
||||
"object-src 'none'", // Block Flash/plugins (defense-in-depth)
|
||||
"frame-src https://www.google.com", // Allow Google Maps iframe embed
|
||||
"frame-src https://www.google.com https://api.wrnexus.com", // Google Maps iframe + WRNexus bot widget iframe
|
||||
"frame-ancestors 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
|
||||
Reference in New Issue
Block a user