@networkpro/web 1.11.0 → 1.12.2
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/CHANGELOG.md +66 -0
- package/CHANGELOG.template.md +63 -0
- package/LICENSE.md +19 -8
- package/package.json +2 -2
- package/src/lib/pages/LicenseContent.svelte +22 -3
- package/src/lib/pages/PrivacyContent.svelte +67 -93
- package/src/lib/pages/PrivacyDashboard.svelte +66 -89
- package/src/lib/stores/posthog.js +8 -8
- package/src/lib/stores/trackingPreferences.js +222 -0
- package/src/lib/styles/css/default.css +12 -0
- package/src/lib/styles/global.min.css +1 -1
- package/src/routes/+layout.svelte +0 -2
- package/static/docs/tracking.md +63 -0
- package/static/img/fb-banner.png +0 -0
- package/svelte.config.js +1 -1
- package/CODE_OF_CONDUCT.md +0 -173
- package/src/lib/stores/trackingStatus.js +0 -30
- package/src/lib/utils/privacy.js +0 -81
- package/src/lib/utils/trackingCookies.js +0 -70
- package/src/lib/utils/trackingStatus.js +0 -61
|
@@ -9,28 +9,17 @@ This file is part of Network Pro.
|
|
|
9
9
|
<script>
|
|
10
10
|
import { base } from "$app/paths";
|
|
11
11
|
import { onMount } from "svelte";
|
|
12
|
-
import { trackingStatus } from "$lib/stores/trackingStatus.js";
|
|
13
|
-
import {
|
|
14
|
-
/** @type {(type: 'enable' | 'disable') => void} */
|
|
15
|
-
setTrackingPreference,
|
|
16
|
-
/** @type {() => void} */
|
|
17
|
-
clearTrackingPreferences,
|
|
18
|
-
} from "$lib/utils/trackingCookies.js";
|
|
19
|
-
import { CONSTANTS } from "$lib";
|
|
20
|
-
|
|
21
|
-
console.log(CONSTANTS.COMPANY_INFO.APP_NAME);
|
|
22
12
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
13
|
+
import {
|
|
14
|
+
trackingPreferences,
|
|
15
|
+
setOptIn,
|
|
16
|
+
setOptOut,
|
|
17
|
+
clearPrefs,
|
|
18
|
+
} from "$lib/stores/trackingPreferences.js";
|
|
28
19
|
|
|
29
|
-
|
|
30
|
-
const NAV = CONSTANTS.NAV;
|
|
20
|
+
import { CONSTANTS } from "$lib";
|
|
31
21
|
|
|
32
|
-
|
|
33
|
-
const spaceStyle = "spacer";
|
|
22
|
+
const { CONTACT, PAGE, NAV } = CONSTANTS;
|
|
34
23
|
|
|
35
24
|
/** @type {string} */
|
|
36
25
|
const privacyPolicy = `${base}/privacy`;
|
|
@@ -41,70 +30,55 @@ This file is part of Network Pro.
|
|
|
41
30
|
/** @type {string} */
|
|
42
31
|
const classSmall = "small-text";
|
|
43
32
|
|
|
33
|
+
/** @type {string} */
|
|
34
|
+
const spaceStyle = "spacer";
|
|
35
|
+
|
|
44
36
|
/** @type {boolean} */
|
|
45
|
-
let optedOut
|
|
37
|
+
let optedOut;
|
|
46
38
|
|
|
47
39
|
/** @type {boolean} */
|
|
48
|
-
let optedIn
|
|
40
|
+
let optedIn;
|
|
49
41
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
* Uses dynamic import to prevent SSR from loading browser-only dependencies.
|
|
53
|
-
*
|
|
54
|
-
* @returns {Promise<void>}
|
|
55
|
-
*/
|
|
56
|
-
async function refreshTrackingStatus() {
|
|
57
|
-
/** @type {typeof import("$lib/utils/trackingStatus.js")} */
|
|
58
|
-
const tracking = await import("$lib/utils/trackingStatus.js");
|
|
59
|
-
|
|
60
|
-
const prefs = tracking.getTrackingPreferences();
|
|
61
|
-
optedOut = prefs.optedOut;
|
|
62
|
-
optedIn = prefs.optedIn;
|
|
63
|
-
trackingStatus.set(prefs.status);
|
|
64
|
-
}
|
|
42
|
+
/** @type {string} */
|
|
43
|
+
let trackingStatus;
|
|
65
44
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
*/
|
|
69
|
-
onMount(() => {
|
|
70
|
-
refreshTrackingStatus();
|
|
71
|
-
});
|
|
45
|
+
// Reactive assignments from the store
|
|
46
|
+
$: ({ optedOut, optedIn, status: trackingStatus } = $trackingPreferences);
|
|
72
47
|
|
|
73
48
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* @param {boolean} value - Whether the user is opting out
|
|
77
|
-
* @returns {void}
|
|
49
|
+
* Toggle tracking opt-out setting.
|
|
50
|
+
* @param {boolean} value
|
|
78
51
|
*/
|
|
79
52
|
function toggleTracking(value) {
|
|
80
|
-
|
|
81
|
-
if (optedOut) {
|
|
53
|
+
if (value) {
|
|
82
54
|
console.log("[Tracking] User opted out");
|
|
83
|
-
|
|
55
|
+
setOptOut();
|
|
84
56
|
} else {
|
|
85
57
|
console.log("[Tracking] User cleared opt-out");
|
|
86
|
-
|
|
58
|
+
clearPrefs();
|
|
87
59
|
}
|
|
88
|
-
refreshTrackingStatus();
|
|
89
60
|
}
|
|
90
61
|
|
|
91
62
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* @param {boolean} value - Whether the user is opting in
|
|
95
|
-
* @returns {void}
|
|
63
|
+
* Toggle tracking opt-in setting.
|
|
64
|
+
* @param {boolean} value
|
|
96
65
|
*/
|
|
97
66
|
function toggleOptIn(value) {
|
|
98
|
-
|
|
99
|
-
if (optedIn) {
|
|
67
|
+
if (value) {
|
|
100
68
|
console.log("[Tracking] User opted in");
|
|
101
|
-
|
|
69
|
+
setOptIn();
|
|
102
70
|
} else {
|
|
103
71
|
console.log("[Tracking] User cleared opt-in");
|
|
104
|
-
|
|
72
|
+
clearPrefs();
|
|
105
73
|
}
|
|
106
|
-
refreshTrackingStatus();
|
|
107
74
|
}
|
|
75
|
+
|
|
76
|
+
onMount(() => {
|
|
77
|
+
console.log(
|
|
78
|
+
"[PrivacyDashboard] Tracking status:",
|
|
79
|
+
$trackingPreferences.status,
|
|
80
|
+
);
|
|
81
|
+
});
|
|
108
82
|
</script>
|
|
109
83
|
|
|
110
84
|
<section id="top">
|
|
@@ -167,43 +141,46 @@ This file is part of Network Pro.
|
|
|
167
141
|
|
|
168
142
|
|
|
169
143
|
|
|
170
|
-
{#if
|
|
144
|
+
{#if trackingStatus && trackingStatus !== "⏳ Checking tracking preferences..."}
|
|
171
145
|
<p id="tracking-status" aria-live="polite">
|
|
172
146
|
<strong>Tracking Status:</strong>
|
|
173
|
-
{
|
|
147
|
+
{trackingStatus}
|
|
174
148
|
</p>
|
|
175
149
|
{:else}
|
|
176
150
|
<p id="tracking-status" aria-live="polite">
|
|
177
|
-
<strong>Tracking Status:</strong>
|
|
178
|
-
<em>Loading…</em>
|
|
151
|
+
<strong>Tracking Status:</strong> <em>Loading…</em>
|
|
179
152
|
</p>
|
|
180
153
|
{/if}
|
|
181
154
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
155
|
+
<fieldset>
|
|
156
|
+
<legend class="sr-only">Tracking Preference Controls</legend>
|
|
157
|
+
|
|
158
|
+
<!-- Opt-out checkbox -->
|
|
159
|
+
<label>
|
|
160
|
+
<input
|
|
161
|
+
type="checkbox"
|
|
162
|
+
checked={optedOut}
|
|
163
|
+
disabled={optedIn}
|
|
164
|
+
aria-describedby="tracking-status"
|
|
165
|
+
on:change={(e) =>
|
|
166
|
+
toggleTracking(/** @type {HTMLInputElement} */ (e.target).checked)} />
|
|
167
|
+
<strong> Disable analytics tracking (opt-out)</strong>
|
|
168
|
+
</label>
|
|
169
|
+
|
|
170
|
+
<br />
|
|
171
|
+
|
|
172
|
+
<!-- Opt-in checkbox -->
|
|
173
|
+
<label>
|
|
174
|
+
<input
|
|
175
|
+
type="checkbox"
|
|
176
|
+
checked={optedIn}
|
|
177
|
+
disabled={optedOut}
|
|
178
|
+
aria-describedby="tracking-status"
|
|
179
|
+
on:change={(e) =>
|
|
180
|
+
toggleOptIn(/** @type {HTMLInputElement} */ (e.target).checked)} />
|
|
181
|
+
<strong> Enable analytics tracking (opt-in)</strong>
|
|
182
|
+
</label>
|
|
183
|
+
</fieldset>
|
|
207
184
|
|
|
208
185
|
<div class={spaceStyle}></div>
|
|
209
186
|
|
|
@@ -12,9 +12,9 @@ SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from "$lib/
|
|
15
|
+
remindUserToReconsent,
|
|
16
|
+
trackingPreferences,
|
|
17
|
+
} from "$lib/stores/trackingPreferences.js";
|
|
18
18
|
import { get, writable } from "svelte/store";
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -44,11 +44,11 @@ export async function initPostHog() {
|
|
|
44
44
|
if (initialized || typeof window === "undefined") return;
|
|
45
45
|
initialized = true;
|
|
46
46
|
|
|
47
|
-
const
|
|
48
|
-
trackingEnabled.set(
|
|
49
|
-
showReminder.set(
|
|
47
|
+
const { enabled } = get(trackingPreferences);
|
|
48
|
+
trackingEnabled.set(enabled);
|
|
49
|
+
showReminder.set(get(remindUserToReconsent)); // ✅ use derived store instead
|
|
50
50
|
|
|
51
|
-
if (!
|
|
51
|
+
if (!enabled) {
|
|
52
52
|
console.log("[PostHog] Tracking is disabled — skipping init.");
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
@@ -63,7 +63,7 @@ export async function initPostHog() {
|
|
|
63
63
|
capture_pageview: false,
|
|
64
64
|
person_profiles: "identified_only",
|
|
65
65
|
loaded: (phInstance) => {
|
|
66
|
-
if (!
|
|
66
|
+
if (!enabled) {
|
|
67
67
|
console.log(
|
|
68
68
|
"[PostHog] ⛔ User opted out — calling opt_out_capturing()",
|
|
69
69
|
);
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
src/lib/stores/trackingPreferences.js
|
|
3
|
+
|
|
4
|
+
Copyright © 2025 Network Pro Strategies (Network Pro™)
|
|
5
|
+
SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
|
|
6
|
+
This file is part of Network Pro.
|
|
7
|
+
========================================================================== */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* NOTE: Legacy logic from `trackingCookies.js` was merged here in June 2025.
|
|
11
|
+
* That file has been removed to consolidate stateful tracking logic into a
|
|
12
|
+
* reactive store with SSR compatibility.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @file trackingPreferences.js
|
|
17
|
+
* @description Reactive store for tracking preferences derived from
|
|
18
|
+
* cookies and browser signals (DNT / GPC). Safe for SSR.
|
|
19
|
+
* @module src/lib/stores
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { browser } from "$app/environment";
|
|
23
|
+
import { derived, writable } from "svelte/store";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {object} TrackingState
|
|
27
|
+
* @property {boolean} optedIn - User explicitly opted in via cookie
|
|
28
|
+
* @property {boolean} optedOut - User explicitly opted out via cookie
|
|
29
|
+
* @property {boolean} dnt - Do Not Track browser signal
|
|
30
|
+
* @property {boolean} gpc - Global Privacy Control browser signal
|
|
31
|
+
* @property {boolean} enabled - Whether tracking is permitted
|
|
32
|
+
* @property {string} status - Human-readable description of tracking state
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @returns {string} Raw document.cookie or empty string (SSR-safe)
|
|
37
|
+
*/
|
|
38
|
+
function readCookies() {
|
|
39
|
+
return browser ? document.cookie || "" : "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if a specific cookie exists
|
|
44
|
+
* @param {string} name
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
function cookieExists(name) {
|
|
48
|
+
return readCookies().includes(`${name}=true`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Set a cookie with boolean true, 1-year duration
|
|
53
|
+
* @param {string} name
|
|
54
|
+
* @returns {void}
|
|
55
|
+
*/
|
|
56
|
+
function setCookie(name) {
|
|
57
|
+
if (browser) {
|
|
58
|
+
document.cookie = `${name}=true; path=/; max-age=31536000; samesite=lax`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Remove a cookie by setting zero max-age
|
|
64
|
+
* @param {string} name
|
|
65
|
+
* @returns {void}
|
|
66
|
+
*/
|
|
67
|
+
function removeCookie(name) {
|
|
68
|
+
if (browser) {
|
|
69
|
+
document.cookie = `${name}=; Max-Age=0; path=/; samesite=lax`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Write tracking_consent_timestamp cookie
|
|
75
|
+
* @returns {void}
|
|
76
|
+
*/
|
|
77
|
+
function setConsentTimestamp() {
|
|
78
|
+
if (browser) {
|
|
79
|
+
document.cookie = `tracking_consent_timestamp=${Date.now()}; path=/; max-age=31536000; samesite=lax`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Remove tracking_consent_timestamp cookie
|
|
85
|
+
* @returns {void}
|
|
86
|
+
*/
|
|
87
|
+
function removeConsentTimestamp() {
|
|
88
|
+
if (browser) {
|
|
89
|
+
document.cookie = `tracking_consent_timestamp=; Max-Age=0; path=/; samesite=lax`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @returns {{ dnt: boolean, gpc: boolean }}
|
|
95
|
+
*/
|
|
96
|
+
function getPrivacySignals() {
|
|
97
|
+
if (!browser) return { dnt: false, gpc: false };
|
|
98
|
+
|
|
99
|
+
const dnt = navigator.doNotTrack === "1";
|
|
100
|
+
// @ts-expect-error: Non-standard GPC property
|
|
101
|
+
const gpc = navigator.globalPrivacyControl === true;
|
|
102
|
+
|
|
103
|
+
return { dnt, gpc };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {object} args
|
|
108
|
+
* @param {boolean} args.optedOut
|
|
109
|
+
* @param {boolean} args.optedIn
|
|
110
|
+
* @param {boolean} args.dnt
|
|
111
|
+
* @param {boolean} args.gpc
|
|
112
|
+
* @returns {string}
|
|
113
|
+
*/
|
|
114
|
+
function deriveStatus({ optedOut, optedIn, dnt, gpc }) {
|
|
115
|
+
if (optedOut) return "🔒 Tracking disabled (manual opt-out)";
|
|
116
|
+
if (optedIn) return "✅ Tracking enabled (manual opt-in)";
|
|
117
|
+
if (dnt || gpc) return "🛑 Tracking disabled (via browser signal)";
|
|
118
|
+
return "⚙️ Using default settings (tracking enabled)";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @returns {TrackingState}
|
|
123
|
+
*/
|
|
124
|
+
function computePreferences() {
|
|
125
|
+
const optedOut = cookieExists("disable_tracking");
|
|
126
|
+
const optedIn = cookieExists("enable_tracking");
|
|
127
|
+
const { dnt, gpc } = getPrivacySignals();
|
|
128
|
+
|
|
129
|
+
const enabled = optedIn || (!optedOut && !dnt && !gpc);
|
|
130
|
+
const status = deriveStatus({ optedOut, optedIn, dnt, gpc });
|
|
131
|
+
|
|
132
|
+
return { optedIn, optedOut, dnt, gpc, enabled, status };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- Writable store ---
|
|
136
|
+
/** @type {import('svelte/store').Writable<TrackingState>} */
|
|
137
|
+
export const trackingPreferences = writable(
|
|
138
|
+
browser
|
|
139
|
+
? computePreferences()
|
|
140
|
+
: {
|
|
141
|
+
optedIn: false,
|
|
142
|
+
optedOut: false,
|
|
143
|
+
dnt: false,
|
|
144
|
+
gpc: false,
|
|
145
|
+
enabled: false,
|
|
146
|
+
status: "⏳ Checking tracking preferences...",
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Returns true if the user manually set a tracking preference cookie.
|
|
152
|
+
* @returns {boolean}
|
|
153
|
+
*/
|
|
154
|
+
function hasUserManuallySetTrackingPreference() {
|
|
155
|
+
const cookies = readCookies();
|
|
156
|
+
return (
|
|
157
|
+
cookies.includes("enable_tracking=true") ||
|
|
158
|
+
cookies.includes("disable_tracking=true")
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Determines if user should be reminded to reconsent (after 6 months).
|
|
164
|
+
*
|
|
165
|
+
* @param {TrackingState} $prefs - The current tracking preferences.
|
|
166
|
+
* @returns {boolean}
|
|
167
|
+
* @type {import("svelte/store").Readable<boolean>}
|
|
168
|
+
*/
|
|
169
|
+
export const remindUserToReconsent = derived(trackingPreferences, (_prefs) => {
|
|
170
|
+
if (!browser) return false;
|
|
171
|
+
if (!hasUserManuallySetTrackingPreference()) return false;
|
|
172
|
+
|
|
173
|
+
const match = readCookies().match(/tracking_consent_timestamp=(\d+)/);
|
|
174
|
+
if (!match) return true;
|
|
175
|
+
|
|
176
|
+
const timestamp = Number(match[1]);
|
|
177
|
+
if (isNaN(timestamp)) return true;
|
|
178
|
+
|
|
179
|
+
const age = Date.now() - timestamp;
|
|
180
|
+
return age > 1000 * 60 * 60 * 24 * 180; // 6 months
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Force-refresh current preferences
|
|
185
|
+
* @returns {void}
|
|
186
|
+
*/
|
|
187
|
+
export function refreshTrackingPreferences() {
|
|
188
|
+
if (browser) trackingPreferences.set(computePreferences());
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Enable tracking by setting opt-in cookie
|
|
193
|
+
* @returns {void}
|
|
194
|
+
*/
|
|
195
|
+
export function setOptIn() {
|
|
196
|
+
setCookie("enable_tracking");
|
|
197
|
+
removeCookie("disable_tracking");
|
|
198
|
+
setConsentTimestamp();
|
|
199
|
+
refreshTrackingPreferences();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Disable tracking by setting opt-out cookie
|
|
204
|
+
* @returns {void}
|
|
205
|
+
*/
|
|
206
|
+
export function setOptOut() {
|
|
207
|
+
setCookie("disable_tracking");
|
|
208
|
+
removeCookie("enable_tracking");
|
|
209
|
+
setConsentTimestamp();
|
|
210
|
+
refreshTrackingPreferences();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Clear all tracking preference cookies
|
|
215
|
+
* @returns {void}
|
|
216
|
+
*/
|
|
217
|
+
export function clearPrefs() {
|
|
218
|
+
removeCookie("enable_tracking");
|
|
219
|
+
removeCookie("disable_tracking");
|
|
220
|
+
removeConsentTimestamp();
|
|
221
|
+
refreshTrackingPreferences();
|
|
222
|
+
}
|
|
@@ -506,3 +506,15 @@ footer .container {
|
|
|
506
506
|
font-family: inherit; /* Ensure it uses the same font-family as normal text */
|
|
507
507
|
font-style: normal; /* Remove italic for the description */
|
|
508
508
|
}
|
|
509
|
+
|
|
510
|
+
.sr-only {
|
|
511
|
+
position: absolute;
|
|
512
|
+
width: 1px;
|
|
513
|
+
height: 1px;
|
|
514
|
+
padding: 0;
|
|
515
|
+
margin: -1px;
|
|
516
|
+
border: 0;
|
|
517
|
+
clip: rect(0, 0, 0, 0);
|
|
518
|
+
overflow: hidden;
|
|
519
|
+
white-space: nowrap;
|
|
520
|
+
}
|
|
@@ -3,4 +3,4 @@ Copyright © 2025 Network Pro Strategies (Network Pro™)
|
|
|
3
3
|
SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
|
|
4
4
|
This file is part of Network Pro.
|
|
5
5
|
========================================================================== */
|
|
6
|
-
html{-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{margin:.67em 0;font-size:2em}hr{box-sizing:content-box}pre{font-family:monospace;font-size:1em}a{background-color:#0000}abbr[title]{border-bottom:none;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted buttontext}fieldset{padding:.35em .75em .625em}legend{color:inherit;box-sizing:border-box;white-space:normal;max-width:100%;padding:0;display:table}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}details{display:block}summary{display:list-item}template{display:none}html{color:#222;scroll-behavior:smooth;font-size:1em;line-height:1.4}::-moz-selection{text-shadow:none;background:#191919}::selection{text-shadow:none;background:#191919}hr{border:0;border-top:1px solid #ccc;height:1px;margin:1em 0;padding:0;display:block;overflow:visible}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}body{color:#fafafa;background-color:#191919;margin:10px;font-family:Arial,Helvetica,sans-serif}a{text-decoration:none}a:link{color:#ffc627}a:hover,a:active{color:#ffc627;text-decoration:underline}a:focus{color:#191919;background-color:#ffc627}a:visited,a:visited:hover{color:#7f6227}.hidden,[hidden]{display:none!important}.visually-hidden{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.visually-hidden.focusable:active,.visually-hidden.focusable:focus{clip:auto;width:auto;height:auto;white-space:inherit;margin:0;position:static;overflow:visible}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:"";display:table}.clearfix:after{clear:both}@media print{*,:before,:after{color:#000!important;box-shadow:none!important;text-shadow:none!important;background:#fff!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^=\#]:after,a[href^=javascript\:]:after{content:""}pre{white-space:pre-wrap!important}pre,blockquote{page-break-inside:avoid;border:1px solid #999}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}.full-width-section{background-position:50%;background-size:cover;width:100%;max-width:1920px;margin:0 auto}.container{max-width:1200px;margin:0 auto;padding:0 12px}.readable{max-width:900px;margin:0 auto}header,footer{width:100%}header .container,footer .container{max-width:1200px;margin:0 auto;padding:20px 12px}.gh{border-collapse:collapse;border-spacing:0;margin:0 auto}.gh td,.gh th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.gh .gh-tcell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.gh,.gh col{width:auto!important}.gh-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.soc{border-collapse:collapse;border-spacing:0;margin:0 auto}.soc td,.soc th{border-collapse:collapse;word-break:normal;padding:8px;overflow:hidden}.soc .soc-fa{text-align:center;vertical-align:middle}@media screen and (width<=767px){.soc,.soc col{width:auto!important}.soc-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.foss{border-collapse:collapse;border-spacing:0}.foss td,.foss th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.foss .foss-cell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.foss,.foss col{width:auto!important}.foss-wrap{-webkit-overflow-scrolling:touch;overflow-x:auto}}.bnav{text-align:center;border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav td,.bnav th{text-align:center;vertical-align:middle;word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav .bnav-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav,.bnav col{width:auto!important}.bnav-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.bnav2{border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav2 td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav2 th{word-break:normal;border-style:none;padding:12px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.bnav2 .bnav2-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav2,.bnav2 col{width:auto!important}.bnav2-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.pgp{border-collapse:collapse;border-spacing:0;margin:0 auto}.pgp td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp th{word-break:normal;border:1px solid #000;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp .pgp-col1{text-align:right;vertical-align:middle;padding-right:1rem}.pgp .pgp-col2{text-align:left;vertical-align:middle;padding-left:1rem}@media screen and (width<=767px){.pgp,.pgp col{width:auto!important}.pgp-wrap{-webkit-overflow-scrolling:touch;margin:2rem 0 auto;overflow-x:auto}}.logo{margin-left:auto;margin-right:auto;display:block}.index-title1{text-align:center;font-style:italic;font-weight:700}.index-title2{letter-spacing:-.015em;text-align:center;font-variant:small-caps;font-size:1.25rem;line-height:1.625rem}.index1{letter-spacing:-.035em;text-align:center;font-style:italic;font-weight:700;line-height:2.125rem}.index2{letter-spacing:-.035em;text-align:center;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.index3{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem}.index4{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem;text-decoration:underline}.subhead{letter-spacing:-.035em;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.bold{font-weight:700}.emphasis{font-style:italic}.uline{text-decoration:underline}.bolditalic{font-style:italic;font-weight:700}.bquote{border-left:3px solid #9e9e9e;margin-left:30px;padding-left:10px;font-style:italic}.small-text{font-size:.75rem;line-height:1.125rem}.large-text-center{text-align:center;font-size:1.25rem;line-height:1.75rem}.prewrap{white-space:pre-wrap;display:block}.hr-styled{width:75%;margin:auto}.center-text{text-align:center}.copyright{text-align:center;font-size:.75rem;line-height:1.125rem}.gold{color:#ffc627}.visited{color:#7f6227}.goldseparator{color:#ffc627;margin:0 .5rem}.center-nav{text-align:center;padding:5px;font-size:1rem;line-height:1.5rem}.block{resize:none;background:0 0;border:none;border-radius:0;outline:none;width:100%;font-size:.75rem;line-height:1.125rem}.fingerprint{white-space:pre-line;font-weight:700;display:block}.pgp-image{width:150px;height:150px}.spacer{margin:2rem 0}.separator{margin:0 .5rem}.emoji{margin-right:8px}.headline{margin-bottom:4px;font-style:italic;font-weight:700;display:block}.label{font-family:inherit;font-weight:700}.description{font-family:inherit;font-style:normal;font-weight:400;display:inline}
|
|
6
|
+
html{-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{margin:.67em 0;font-size:2em}hr{box-sizing:content-box}pre{font-family:monospace;font-size:1em}a{background-color:#0000}abbr[title]{border-bottom:none;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted buttontext}fieldset{padding:.35em .75em .625em}legend{color:inherit;box-sizing:border-box;white-space:normal;max-width:100%;padding:0;display:table}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}details{display:block}summary{display:list-item}template{display:none}html{color:#222;scroll-behavior:smooth;font-size:1em;line-height:1.4}::-moz-selection{text-shadow:none;background:#191919}::selection{text-shadow:none;background:#191919}hr{border:0;border-top:1px solid #ccc;height:1px;margin:1em 0;padding:0;display:block;overflow:visible}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}body{color:#fafafa;background-color:#191919;margin:10px;font-family:Arial,Helvetica,sans-serif}a{text-decoration:none}a:link{color:#ffc627}a:hover,a:active{color:#ffc627;text-decoration:underline}a:focus{color:#191919;background-color:#ffc627}a:visited,a:visited:hover{color:#7f6227}.hidden,[hidden]{display:none!important}.visually-hidden{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.visually-hidden.focusable:active,.visually-hidden.focusable:focus{clip:auto;width:auto;height:auto;white-space:inherit;margin:0;position:static;overflow:visible}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:"";display:table}.clearfix:after{clear:both}@media print{*,:before,:after{color:#000!important;box-shadow:none!important;text-shadow:none!important;background:#fff!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^=\#]:after,a[href^=javascript\:]:after{content:""}pre{white-space:pre-wrap!important}pre,blockquote{page-break-inside:avoid;border:1px solid #999}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}.full-width-section{background-position:50%;background-size:cover;width:100%;max-width:1920px;margin:0 auto}.container{max-width:1200px;margin:0 auto;padding:0 12px}.readable{max-width:900px;margin:0 auto}header,footer{width:100%}header .container,footer .container{max-width:1200px;margin:0 auto;padding:20px 12px}.gh{border-collapse:collapse;border-spacing:0;margin:0 auto}.gh td,.gh th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.gh .gh-tcell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.gh,.gh col{width:auto!important}.gh-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.soc{border-collapse:collapse;border-spacing:0;margin:0 auto}.soc td,.soc th{border-collapse:collapse;word-break:normal;padding:8px;overflow:hidden}.soc .soc-fa{text-align:center;vertical-align:middle}@media screen and (width<=767px){.soc,.soc col{width:auto!important}.soc-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.foss{border-collapse:collapse;border-spacing:0}.foss td,.foss th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.foss .foss-cell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.foss,.foss col{width:auto!important}.foss-wrap{-webkit-overflow-scrolling:touch;overflow-x:auto}}.bnav{text-align:center;border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav td,.bnav th{text-align:center;vertical-align:middle;word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav .bnav-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav,.bnav col{width:auto!important}.bnav-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.bnav2{border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav2 td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav2 th{word-break:normal;border-style:none;padding:12px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.bnav2 .bnav2-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav2,.bnav2 col{width:auto!important}.bnav2-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.pgp{border-collapse:collapse;border-spacing:0;margin:0 auto}.pgp td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp th{word-break:normal;border:1px solid #000;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp .pgp-col1{text-align:right;vertical-align:middle;padding-right:1rem}.pgp .pgp-col2{text-align:left;vertical-align:middle;padding-left:1rem}@media screen and (width<=767px){.pgp,.pgp col{width:auto!important}.pgp-wrap{-webkit-overflow-scrolling:touch;margin:2rem 0 auto;overflow-x:auto}}.logo{margin-left:auto;margin-right:auto;display:block}.index-title1{text-align:center;font-style:italic;font-weight:700}.index-title2{letter-spacing:-.015em;text-align:center;font-variant:small-caps;font-size:1.25rem;line-height:1.625rem}.index1{letter-spacing:-.035em;text-align:center;font-style:italic;font-weight:700;line-height:2.125rem}.index2{letter-spacing:-.035em;text-align:center;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.index3{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem}.index4{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem;text-decoration:underline}.subhead{letter-spacing:-.035em;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.bold{font-weight:700}.emphasis{font-style:italic}.uline{text-decoration:underline}.bolditalic{font-style:italic;font-weight:700}.bquote{border-left:3px solid #9e9e9e;margin-left:30px;padding-left:10px;font-style:italic}.small-text{font-size:.75rem;line-height:1.125rem}.large-text-center{text-align:center;font-size:1.25rem;line-height:1.75rem}.prewrap{white-space:pre-wrap;display:block}.hr-styled{width:75%;margin:auto}.center-text{text-align:center}.copyright{text-align:center;font-size:.75rem;line-height:1.125rem}.gold{color:#ffc627}.visited{color:#7f6227}.goldseparator{color:#ffc627;margin:0 .5rem}.center-nav{text-align:center;padding:5px;font-size:1rem;line-height:1.5rem}.block{resize:none;background:0 0;border:none;border-radius:0;outline:none;width:100%;font-size:.75rem;line-height:1.125rem}.fingerprint{white-space:pre-line;font-weight:700;display:block}.pgp-image{width:150px;height:150px}.spacer{margin:2rem 0}.separator{margin:0 .5rem}.emoji{margin-right:8px}.headline{margin-bottom:4px;font-style:italic;font-weight:700;display:block}.label{font-family:inherit;font-weight:700}.description{font-family:inherit;font-style:normal;font-weight:400;display:inline}.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}
|
|
@@ -14,7 +14,6 @@ This file is part of Network Pro.
|
|
|
14
14
|
import { initPostHog, showReminder, capture } from "$lib/stores/posthog";
|
|
15
15
|
import { registerServiceWorker } from "$lib/registerServiceWorker.js";
|
|
16
16
|
import { browser } from "$app/environment";
|
|
17
|
-
import { shouldTrackUser } from "$lib/utils/privacy.js";
|
|
18
17
|
|
|
19
18
|
import ContainerSection from "$lib/components/ContainerSection.svelte";
|
|
20
19
|
import Footer from "$lib/components/layout/Footer.svelte";
|
|
@@ -54,7 +53,6 @@ This file is part of Network Pro.
|
|
|
54
53
|
console.log("ENV MODE =", import.meta.env.MODE);
|
|
55
54
|
console.log("isDev =", isDev);
|
|
56
55
|
console.log("debug param =", debug);
|
|
57
|
-
console.log("shouldTrackUser =", shouldTrackUser()); // Now called statically
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
// Preload logo assets
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Tracking Preferences & Privacy Signals
|
|
2
|
+
|
|
3
|
+
<!-- markdownlint-disable MD018 -->
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This document explains how Network Pro handles analytics tracking in a privacy-aware, user-consented, and standards-compliant manner. It covers:
|
|
8
|
+
|
|
9
|
+
- Tracking preference storage (cookies)
|
|
10
|
+
- Browser signals (DNT and GPC)
|
|
11
|
+
- Reconsent logic
|
|
12
|
+
- Reactive store architecture
|
|
13
|
+
|
|
14
|
+
### 🔐 Principles
|
|
15
|
+
|
|
16
|
+
- **Privacy by default**: Tracking is disabled when browser signals indicate user preference (DNT/GPC).
|
|
17
|
+
- **Explicit consent**: Users may opt-in or opt-out manually, overriding signals.
|
|
18
|
+
- **Persistent choice**: Consent state is remembered via first-party cookies.
|
|
19
|
+
- **Transparency**: The tracking status is shown to users in the UI.
|
|
20
|
+
|
|
21
|
+
### 🧠 Architecture Summary
|
|
22
|
+
|
|
23
|
+
- **Store**: `src/lib/stores/trackingPreferences.js`
|
|
24
|
+
- Consolidates logic for cookie preferences, browser signals, and consent state.
|
|
25
|
+
- SSR-safe, reactive, and fully declarative.
|
|
26
|
+
- **Deprecated**:
|
|
27
|
+
- `utils/privacy.js` → replaced by derived store logic.
|
|
28
|
+
- `utils/trackingCookies.js` → merged into the store with SSR-safe cookie APIs.
|
|
29
|
+
|
|
30
|
+
### Reactive State
|
|
31
|
+
|
|
32
|
+
| Store | Type | Description |
|
|
33
|
+
| ----------------------- | ------------------------- | ----------------------------------------------------------------------- |
|
|
34
|
+
| `trackingPreferences` | `Readable<TrackingState>` | Contains current tracking metadata (opt-in/out, DNT, GPC, status, etc). |
|
|
35
|
+
| `trackingEnabled` | `Writable<boolean>` | Exposed to toggle or query PostHog tracking state reactively. |
|
|
36
|
+
| `remindUserToReconsent` | `Readable<boolean>` | Indicates whether a consent renewal prompt should be shown. |
|
|
37
|
+
| `showReminder` | `Writable<boolean>` | Used by PostHog to conditionally display a reminder or banner. |
|
|
38
|
+
|
|
39
|
+
### ⏳ Reconsent Logic
|
|
40
|
+
|
|
41
|
+
The derived store `remindUserToReconsent` evaluates whether a user should be reminded to re-consent to tracking.
|
|
42
|
+
|
|
43
|
+
It checks for:
|
|
44
|
+
|
|
45
|
+
- Manual opt-in or opt-out
|
|
46
|
+
- A valid `tracking_consent_timestamp` cookie
|
|
47
|
+
- Whether 6+ months have elapsed since that timestamp
|
|
48
|
+
|
|
49
|
+
### ⚙️ Developer Notes
|
|
50
|
+
|
|
51
|
+
- Changes to tracking preferences update cookies and reactive state
|
|
52
|
+
- Reconsent timestamp is written/cleared via store utility functions
|
|
53
|
+
- Use `$trackingPreferences` and `remindUserToReconsent` wherever reactive values are needed
|
|
54
|
+
|
|
55
|
+
### 💡 Related Components
|
|
56
|
+
|
|
57
|
+
| File | Purpose |
|
|
58
|
+
| ------------------------------- | -------------------------------------------------------------------------------- |
|
|
59
|
+
| `+layout.svelte` | Initializes PostHog client and service worker; references `trackingPreferences`. |
|
|
60
|
+
| `PrivacyDashboard.svelte` | UI control panel for opt-in/out toggles and consent status display. |
|
|
61
|
+
| `PrivacyContent.svelte` | Informational content rendered in modals, footers, and standalone pages. |
|
|
62
|
+
| `stores/trackingPreferences.js` | Primary source of truth; tracks and derives tracking state. |
|
|
63
|
+
| `stores/posthog.js` | Encapsulates privacy-safe analytics setup and event capture logic. |
|
|
Binary file
|
package/svelte.config.js
CHANGED
|
@@ -23,7 +23,7 @@ const config = {
|
|
|
23
23
|
kit: {
|
|
24
24
|
// Netlify adapter configuration
|
|
25
25
|
adapter: adapter({
|
|
26
|
-
edge: false, // Disable edge functions
|
|
26
|
+
edge: false, // Disable edge functions (optional, enable if needed)
|
|
27
27
|
split: false, // Disable splitting function files (optional, enable if needed)
|
|
28
28
|
}),
|
|
29
29
|
|