diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 00000000..ed54cead --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,96 @@ +--- +# Codacy Configuration File +# https://docs.codacy.com/repositories-configure/codacy-configuration-file/ + +comment: + enabled: false + +github_checks: + annotations: true + +file_extensions: + - ".ts" + - ".js" + - ".svelte" + - ".scss" + - ".css" + +duplication: + exclude_paths: + - "tests/**" + +engines: + eslint: + config_file: eslintrc.config.js + +# Exclude patterns - ignore test files and build artifacts from analysis +exclude_paths: + - "tests/**" + - "**/*.test.ts" + - "**/*.test.js" + - "**/*.spec.ts" + - "**/*.spec.js" + - ".svelte-kit/**" + - "build/**" + - "dist/**" + - "coverage/**" + - "playwright-report/**" + - "node_modules/**" + - "**/*.config.ts" + - "**/*.config.js" + - "**/*.d.ts" + - "static/**" + - ".github/**" + +# Language-specific engines configuration +engines: + # ESLint configuration (if you want to customize) + eslint: + enabled: true + exclude_paths: + - "tests/**" + + # TypeScript/TSLint + tslint: + enabled: true + exclude_paths: + - "tests/**" + +# Additional configuration +duplication: + enabled: true + exclude_paths: + - "tests/**" + +# Complexity and code style checks +metrics: + enabled: true + exclude_paths: + - "tests/**" + +# Coverage configuration +coverage: + # Enable coverage tracking + enabled: true + + # Exclude test files from coverage reports + exclude_paths: + - "tests/**" + - "**/*.test.ts" + - "**/*.spec.ts" + + # Coverage thresholds (optional but recommended) + status: + # Project-wide coverage threshold + project: + default: + enabled: true + target: 80% + threshold: 10% # Allow 5% decrease + + # Diff/patch coverage threshold (new code in PRs) + patch: + default: + enabled: true + target: 80% + threshold: 20% # Allow 10% decrease for patches diff --git a/.github/docs/app-customization.md b/.github/docs/app-customization.md index d4d2d33b..5055e501 100644 --- a/.github/docs/app-customization.md +++ b/.github/docs/app-customization.md @@ -157,6 +157,21 @@ environment: - **Example:** `NTB_ALLOWED_DNS_SERVERS='8.8.8.8,1.1.1.1,9.9.9.9'` - **Note:** Only used when `NTB_ALLOW_CUSTOM_DNS='false'` +### Analytics Settings + +- **`NTB_ANALYTICS_DOMAIN`** + - **Description:** Domain for analytics tracking (for Plausible or similar) + - **Default:** `networking-toolbox.as93.net` + - **Example:** `NTB_ANALYTICS_DOMAIN='myapp.example.com'` + - **Disable:** Set to `false` to disable analytics: `NTB_ANALYTICS_DOMAIN='false'` + +- **`NTB_ANALYTICS_DSN`** + - **Description:** URL to the analytics script + - **Default:** `https://no-track.as93.net/js/script.js` + - **Example:** `NTB_ANALYTICS_DSN='https://plausible.io/js/script.js'` + - **Disable:** Set to `false` to disable analytics: `NTB_ANALYTICS_DSN='false'` + - **Note:** Analytics is disabled if either `NTB_ANALYTICS_DOMAIN` or `NTB_ANALYTICS_DSN` is set to `false` + --- ## Example Configurations @@ -174,6 +189,8 @@ NTB_HOMEPAGE_LAYOUT='categories' NTB_ALLOW_CUSTOM_DNS='false' NTB_BLOCK_PRIVATE_DNS_IPS='true' NTB_ALLOWED_DNS_SERVERS='8.8.8.8,1.1.1.1' +NTB_ANALYTICS_DOMAIN='false' +NTB_ANALYTICS_DSN='false' ``` ### Full Customization @@ -190,5 +207,15 @@ NTB_DEFAULT_LANGUAGE='en' NTB_SHOW_TIPS_ON_HOMEPAGE='true' NTB_ALLOW_CUSTOM_DNS='true' NTB_BLOCK_PRIVATE_DNS_IPS='true' +NTB_ANALYTICS_DOMAIN='myapp.example.com' +NTB_ANALYTICS_DSN='https://plausible.io/js/script.js' +``` + +### Self-Hosted (No Analytics) +```bash +NTB_SITE_TITLE='Internal Network Tools' +NTB_DEFAULT_THEME='dark' +NTB_ANALYTICS_DOMAIN='false' +NTB_ANALYTICS_DSN='false' ``` diff --git a/src/lib/components/page-specific/about/DeployingSection.svelte b/src/lib/components/page-specific/about/DeployingSection.svelte index e7739e40..ad592635 100644 --- a/src/lib/components/page-specific/about/DeployingSection.svelte +++ b/src/lib/components/page-specific/about/DeployingSection.svelte @@ -52,13 +52,15 @@
You can customize the branding of your instance, by setting a few environment variables. All are optional.
NTB_SITE_NAME - Change sitenameNTB_SITE_TITLE - Change site nameNTB_SITE_DESCRIPTION - Change site taglineNTB_SITE_ICON - Set site iconNTB_HOMEPAGE_LAYOUT - Set homepage layoutNTB_NAVBAR_DISPLAY - Set navbar display optionNTB_DEFAULT_THEME - Set default themeNTB_DEFAULT_LANGUAGE - Set default langNTB_DEFAULT_LANGUAGE - Set default languageNTB_ANALYTICS_DOMAIN - Analytics domain (or set to false to disable)NTB_ANALYTICS_DSN - Analytics script URL (or set to false to disable)
diff --git a/src/lib/config/customizable-settings.ts b/src/lib/config/customizable-settings.ts
index b1cfcbf4..dca21189 100644
--- a/src/lib/config/customizable-settings.ts
+++ b/src/lib/config/customizable-settings.ts
@@ -110,6 +110,31 @@ export const ALLOWED_DNS_SERVERS = env.NTB_ALLOWED_DNS_SERVERS
? env.NTB_ALLOWED_DNS_SERVERS.split(',').map((ip) => ip.trim())
: DEFAULT_TRUSTED_DNS_SERVERS;
+/**
+ * Analytics Settings
+ * Configure analytics tracking for self-hosted instances
+ */
+
+/**
+ * Analytics domain (for Plausible or similar analytics)
+ * Set to 'false' to disable analytics entirely
+ * Default: 'networking-toolbox.as93.net'
+ */
+export const ANALYTICS_DOMAIN = env.NTB_ANALYTICS_DOMAIN ?? 'networking-toolbox.as93.net';
+
+/**
+ * Analytics script URL (for Plausible or similar analytics)
+ * Set to 'false' to disable analytics entirely
+ * Default: 'https://no-track.as93.net/js/script.js'
+ */
+export const ANALYTICS_DSN = env.NTB_ANALYTICS_DSN ?? 'https://no-track.as93.net/js/script.js';
+
+/**
+ * Check if analytics is enabled
+ * Analytics is disabled if either ANALYTICS_DOMAIN or ANALYTICS_DSN is set to 'false'
+ */
+export const ANALYTICS_ENABLED = ANALYTICS_DOMAIN !== 'false' && ANALYTICS_DSN !== 'false';
+
/**
* Get user settings list with values prioritized as:
* 1. User-set value from localStorage
@@ -153,5 +178,7 @@ export function getUserSettingsList(): Array<{ name: string; value: string }> {
{ name: 'NTB_ALLOW_CUSTOM_DNS', value: env.NTB_ALLOW_CUSTOM_DNS || '' },
{ name: 'NTB_BLOCK_PRIVATE_DNS_IPS', value: env.NTB_BLOCK_PRIVATE_DNS_IPS || '' },
{ name: 'NTB_ALLOWED_DNS_SERVERS', value: env.NTB_ALLOWED_DNS_SERVERS || '' },
+ { name: 'NTB_ANALYTICS_DOMAIN', value: env.NTB_ANALYTICS_DOMAIN || '' },
+ { name: 'NTB_ANALYTICS_DSN', value: env.NTB_ANALYTICS_DSN || '' },
];
}
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 8692cd78..1ebff0fa 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -24,6 +24,7 @@
import { ALL_PAGES } from '$lib/constants/nav';
import { initializeOfflineSupport } from '$lib/stores/offline';
import { bookmarks } from '$lib/stores/bookmarks';
+ import { ANALYTICS_ENABLED, ANALYTICS_DOMAIN, ANALYTICS_DSN } from '$lib/config/customizable-settings';
import Header from '$lib/components/furniture/Header.svelte';
import SubHeader from '$lib/components/furniture/SubHeader.svelte';
@@ -400,8 +401,10 @@
})}
{/if}
-
-
+
+ {#if ANALYTICS_ENABLED}
+
+ {/if}
diff --git a/tests/unit/lib/config/customizable-settings.test.ts b/tests/unit/lib/config/customizable-settings.test.ts
new file mode 100644
index 00000000..34ea06f1
--- /dev/null
+++ b/tests/unit/lib/config/customizable-settings.test.ts
@@ -0,0 +1,345 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+
+describe('customizable-settings - Analytics Configuration', () => {
+ beforeEach(() => {
+ vi.resetModules();
+ });
+
+ describe('ANALYTICS_DOMAIN', () => {
+ it('should use default domain when env var is not set', async () => {
+ vi.doMock('$env/dynamic/public', () => ({
+ env: {},
+ }));
+ vi.doMock('$app/environment', () => ({
+ browser: false,
+ }));
+
+ const { ANALYTICS_DOMAIN } = await import('$lib/config/customizable-settings');
+ expect(ANALYTICS_DOMAIN).toBe('networking-toolbox.as93.net');
+ });
+
+ it('should use custom domain when env var is set', async () => {
+ vi.doMock('$env/dynamic/public', () => ({
+ env: {
+ NTB_ANALYTICS_DOMAIN: 'custom-domain.example.com',
+ },
+ }));
+ vi.doMock('$app/environment', () => ({
+ browser: false,
+ }));
+
+ const { ANALYTICS_DOMAIN } = await import('$lib/config/customizable-settings');
+ expect(ANALYTICS_DOMAIN).toBe('custom-domain.example.com');
+ });
+
+ it('should accept "false" as a value to disable analytics', async () => {
+ vi.doMock('$env/dynamic/public', () => ({
+ env: {
+ NTB_ANALYTICS_DOMAIN: 'false',
+ },
+ }));
+ vi.doMock('$app/environment', () => ({
+ browser: false,
+ }));
+
+ const { ANALYTICS_DOMAIN } = await import('$lib/config/customizable-settings');
+ expect(ANALYTICS_DOMAIN).toBe('false');
+ });
+ });
+
+ describe('ANALYTICS_DSN', () => {
+ it('should use default DSN when env var is not set', async () => {
+ vi.doMock('$env/dynamic/public', () => ({
+ env: {},
+ }));
+ vi.doMock('$app/environment', () => ({
+ browser: false,
+ }));
+
+ const { ANALYTICS_DSN } = await import('$lib/config/customizable-settings');
+ expect(ANALYTICS_DSN).toBe('https://no-track.as93.net/js/script.js');
+ });
+
+ it('should use custom DSN when env var is set', async () => {
+ vi.doMock('$env/dynamic/public', () => ({
+ env: {
+ NTB_ANALYTICS_DSN: 'https://plausible.io/js/script.js',
+ },
+ }));
+ vi.doMock('$app/environment', () => ({
+ browser: false,
+ }));
+
+ const { ANALYTICS_DSN } = await import('$lib/config/customizable-settings');
+ expect(ANALYTICS_DSN).toBe('https://plausible.io/js/script.js');
+ });
+
+ it('should accept "false" as a value to disable analytics', async () => {
+ vi.doMock('$env/dynamic/public', () => ({
+ env: {
+ NTB_ANALYTICS_DSN: 'false',
+ },
+ }));
+ vi.doMock('$app/environment', () => ({
+ browser: false,
+ }));
+
+ const { ANALYTICS_DSN } = await import('$lib/config/customizable-settings');
+ expect(ANALYTICS_DSN).toBe('false');
+ });
+ });
+
+ describe('ANALYTICS_ENABLED', () => {
+ it('should be true when both domain and DSN use defaults', async () => {
+ vi.doMock('$env/dynamic/public', () => ({
+ env: {},
+ }));
+ vi.doMock('$app/environment', () => ({
+ browser: false,
+ }));
+
+ const { ANALYTICS_ENABLED } = await import('$lib/config/customizable-settings');
+ expect(ANALYTICS_ENABLED).toBe(true);
+ });
+
+ it('should be true when both domain and DSN are custom values', async () => {
+ vi.doMock('$env/dynamic/public', () => ({
+ env: {
+ NTB_ANALYTICS_DOMAIN: 'custom.example.com',
+ NTB_ANALYTICS_DSN: 'https://analytics.example.com/script.js',
+ },
+ }));
+ vi.doMock('$app/environment', () => ({
+ browser: false,
+ }));
+
+ const { ANALYTICS_ENABLED } = await import('$lib/config/customizable-settings');
+ expect(ANALYTICS_ENABLED).toBe(true);
+ });
+
+ it('should be false when domain is set to "false"', async () => {
+ vi.doMock('$env/dynamic/public', () => ({
+ env: {
+ NTB_ANALYTICS_DOMAIN: 'false',
+ NTB_ANALYTICS_DSN: 'https://no-track.as93.net/js/script.js',
+ },
+ }));
+ vi.doMock('$app/environment', () => ({
+ browser: false,
+ }));
+
+ const { ANALYTICS_ENABLED } = await import('$lib/config/customizable-settings');
+ expect(ANALYTICS_ENABLED).toBe(false);
+ });
+
+ it('should be false when DSN is set to "false"', async () => {
+ vi.doMock('$env/dynamic/public', () => ({
+ env: {
+ NTB_ANALYTICS_DOMAIN: 'networking-toolbox.as93.net',
+ NTB_ANALYTICS_DSN: 'false',
+ },
+ }));
+ vi.doMock('$app/environment', () => ({
+ browser: false,
+ }));
+
+ const { ANALYTICS_ENABLED } = await import('$lib/config/customizable-settings');
+ expect(ANALYTICS_ENABLED).toBe(false);
+ });
+
+ it('should be false when both domain and DSN are set to "false"', async () => {
+ vi.doMock('$env/dynamic/public', () => ({
+ env: {
+ NTB_ANALYTICS_DOMAIN: 'false',
+ NTB_ANALYTICS_DSN: 'false',
+ },
+ }));
+ vi.doMock('$app/environment', () => ({
+ browser: false,
+ }));
+
+ const { ANALYTICS_ENABLED } = await import('$lib/config/customizable-settings');
+ expect(ANALYTICS_ENABLED).toBe(false);
+ });
+ });
+
+ describe('getUserSettingsList - Analytics', () => {
+ it('should include analytics settings in the list', async () => {
+ const mockLocalStorage: Record