From 5df6cec80b04c51f4bab701d215f007e03e647e8 Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Tue, 14 Jul 2026 21:32:42 +0800 Subject: [PATCH 01/37] Fix(api): reject malformed referer instead of throwing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scanners send garbage Referer headers; new URL() threw and surfaced as an unhandled 500. Parse failure now means "not an allowed origin" → 403. Co-Authored-By: Claude Fable 5 --- common/referer-check.js | 9 ++++++++- tests/referer-check.test.js | 10 ++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/common/referer-check.js b/common/referer-check.js index 538353dff..c09606c84 100644 --- a/common/referer-check.js +++ b/common/referer-check.js @@ -6,7 +6,14 @@ function refererCheck(referer) { const allowedDomains = ['localhost', ...(process.env.ALLOWED_DOMAINS || '').split(',')]; if (referer) { - const domain = new URL(referer).hostname; + // Scanners send garbage Referer headers; a parse failure means + // "not an allowed origin", never a thrown 500. + let domain; + try { + domain = new URL(referer).hostname; + } catch { + return false; + } return allowedDomains.includes(domain); } return false; // if no referer is provided, return false diff --git a/tests/referer-check.test.js b/tests/referer-check.test.js index f8cfe94ee..f130a21ee 100644 --- a/tests/referer-check.test.js +++ b/tests/referer-check.test.js @@ -76,9 +76,11 @@ describe('refererCheck — ALLOWED_DOMAINS parsing', () => { }); describe('refererCheck — malformed inputs', () => { - it('throws on non-URL strings (caller must catch)', () => { - // URL constructor throws on garbage; current code doesn't try/catch. - // Documenting the behavior so callers know to validate upstream. - assert.throws(() => refererCheck('not-a-url')); + it('returns false on non-URL strings instead of throwing', () => { + // Scanners send garbage Referer headers; parse failure means denial, + // not an exception bubbling up to a 500. + assert.equal(refererCheck('not-a-url'), false); + assert.equal(refererCheck('http://'), false); + assert.equal(refererCheck('%%%'), false); }); }); From 84d4cd3709dd3658352a3febee68fd12a325a291 Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Tue, 14 Jul 2026 21:32:48 +0800 Subject: [PATCH 02/37] Fix(ui): dedupe MaxMind lookups across concurrent callers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The WebRTC test resolves the same public IP from several STUN servers and each card looked it up independently — N identical /api/maxmind requests. Lookups now share a module-level in-flight map plus a session result cache keyed by ip+lang; null results and rejections stay retryable. Imports switch to relative paths so the Node test runner can load the composable. Co-Authored-By: Claude Fable 5 --- frontend/composables/use-maxmind.js | 76 ++++++++++++++------ tests/composable-maxmind.test.js | 108 ++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 21 deletions(-) create mode 100644 tests/composable-maxmind.test.js diff --git a/frontend/composables/use-maxmind.js b/frontend/composables/use-maxmind.js index 1911b2e11..076967dc3 100644 --- a/frontend/composables/use-maxmind.js +++ b/frontend/composables/use-maxmind.js @@ -3,12 +3,43 @@ // Returns `{ country_code, country, org, asn }` on success. Returns `null` on // every failure path (no MaxMind source, network error, malformed // upstream payload) so callers don't have to disambiguate. +// Lookups are deduped module-wide by ip+lang: the WebRTC test resolves the +// same public IP from several STUN servers and each one triggers a lookup — +// they all share one request, and successful results are cached for the +// session (geo data doesn't change mid-visit). Failures are not cached, so a +// transient error stays retryable. -import { useMainStore } from '@/store'; +// Relative imports (not the @ alias) keep this file importable from the Node +// test runner, same as the other spec-covered composables. +import { useMainStore } from '../store.js'; import { useI18n } from 'vue-i18n'; -import { fetchWithTimeout } from '@/utils/fetch-with-timeout.js'; -import { transformDataFromIPapi } from '@/utils/transform-ip-data.js'; -import getCountryName from '@/data/country-name.js'; +import { fetchWithTimeout } from '../utils/fetch-with-timeout.js'; +import { transformDataFromIPapi } from '../utils/transform-ip-data.js'; +import getCountryName from '../data/country-name.js'; + +// key → resolved result (successes only) / key → in-flight promise. +const lookupCache = new Map(); +const pendingLookups = new Map(); + +// Pure dedup wrapper (exported for tests): concurrent calls with the same key +// await the same doLookup() run; non-null results are cached permanently. +export const dedupedLookup = async (key, doLookup) => { + if (lookupCache.has(key)) return lookupCache.get(key); + const pending = pendingLookups.get(key); + if (pending) return pending; + + const promise = (async () => { + const result = await doLookup(); + if (result !== null) lookupCache.set(key, result); + return result; + })(); + pendingLookups.set(key, promise); + try { + return await promise; + } finally { + pendingLookups.delete(key); + } +}; export function useMaxmind() { const store = useMainStore(); @@ -22,23 +53,26 @@ export function useMaxmind() { // ip-api.com style locale tag — same mapping the legacy call sites used. const apiLang = lang === 'zh' ? 'zh-CN' : lang; - try { - const url = store.getDbUrl(source.id, ip, apiLang); - const response = await fetchWithTimeout(url); - const data = await response.json(); - const ipData = transformDataFromIPapi(data, source.id, t, lang); - if (!ipData) return null; - const country_code = (ipData.country_code || '').toLowerCase(); - const country = country_code - ? getCountryName(ipData.country_code, lang) - : ''; - // Upstream reports unknown ASN as the literal 'N/A' — normalize to ''. - const asn = ipData.asn && ipData.asn !== 'N/A' ? ipData.asn : ''; - return { country_code, country, org: ipData.isp || '', asn }; - } catch (error) { - console.error('useMaxmind lookup failed', error); - return null; - } + // `country` below is localized, so the cache key must carry the lang. + return dedupedLookup(`${ip}|${lang}`, async () => { + try { + const url = store.getDbUrl(source.id, ip, apiLang); + const response = await fetchWithTimeout(url); + const data = await response.json(); + const ipData = transformDataFromIPapi(data, source.id, t, lang); + if (!ipData) return null; + const country_code = (ipData.country_code || '').toLowerCase(); + const country = country_code + ? getCountryName(ipData.country_code, lang) + : ''; + // Upstream reports unknown ASN as the literal 'N/A' — normalize to ''. + const asn = ipData.asn && ipData.asn !== 'N/A' ? ipData.asn : ''; + return { country_code, country, org: ipData.isp || '', asn }; + } catch (error) { + console.error('useMaxmind lookup failed', error); + return null; + } + }); }; return { lookupMaxmind }; diff --git a/tests/composable-maxmind.test.js b/tests/composable-maxmind.test.js new file mode 100644 index 000000000..1130f4883 --- /dev/null +++ b/tests/composable-maxmind.test.js @@ -0,0 +1,108 @@ +// Tests for the module-level lookup dedup in +// frontend/composables/use-maxmind.js: concurrent same-key calls share one +// doLookup run, successful results cache for the session, null results and +// rejections stay retryable. Each test uses its own keys — the cache is +// module-global and never reset. +// +// Importing the composable pulls in the real Pinia store, which needs the +// same globalThis stubs as store.test.js — installed before the import below. + +globalThis.localStorage = { + _data: {}, + getItem(k) { return this._data[k] ?? null; }, + setItem(k, v) { this._data[k] = v; }, + removeItem(k) { delete this._data[k]; }, + clear() { this._data = {}; }, +}; +globalThis.window = { + location: { search: '' }, + addEventListener() {}, + innerWidth: 1024, +}; +globalThis.document = { + addEventListener() {}, + title: '', + querySelector() { return null; }, + // @vue/runtime-dom (pulled in by vue-i18n) creates a diff --git a/frontend/utils/pwa.js b/frontend/utils/pwa.js index db4dba0a2..7632c7136 100644 --- a/frontend/utils/pwa.js +++ b/frontend/utils/pwa.js @@ -13,3 +13,19 @@ export function isRunningAsPwa() { return window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true; } + +// Proactive install-prompt eligibility, checked by App.vue BEFORE mounting +// the PWA widget — ineligible visits never load pwa-install or its manifest +// fetch. Counts this visit as a side effect: the prompt skips the very first +// visit and is capped at 2 shows ever (PWA.vue bumps pwaPopupCount when the +// dialog actually opens). +export const shouldOfferPwaInstall = () => { + try { + const visits = parseInt(localStorage.getItem('pwaVisitCount') || '0', 10) + 1; + localStorage.setItem('pwaVisitCount', visits); + const popups = parseInt(localStorage.getItem('pwaPopupCount') || '0', 10); + return !isRunningAsPwa() && visits >= 2 && popups < 2; + } catch { + return false; // storage disabled — never prompt + } +}; diff --git a/tests/pwa.test.js b/tests/pwa.test.js new file mode 100644 index 000000000..2d87f3ac1 --- /dev/null +++ b/tests/pwa.test.js @@ -0,0 +1,55 @@ +// Tests for the PWA install-prompt eligibility gate (frontend/utils/pwa.js). + +globalThis.localStorage = { + _data: {}, + _throw: false, + getItem(k) { if (this._throw) throw new Error('storage disabled'); return this._data[k] ?? null; }, + setItem(k, v) { if (this._throw) throw new Error('storage disabled'); this._data[k] = String(v); }, + clear() { this._data = {}; this._throw = false; }, +}; +globalThis.window = { + matchMedia: () => ({ matches: globalThis.__pwaDisplayMode }), + navigator: {}, +}; +globalThis.__pwaDisplayMode = false; + +import assert from 'node:assert/strict'; +import { describe, it, beforeEach } from 'node:test'; +import { shouldOfferPwaInstall } from '../frontend/utils/pwa.js'; + +beforeEach(() => { + globalThis.localStorage.clear(); + globalThis.__pwaDisplayMode = false; +}); + +describe('shouldOfferPwaInstall', () => { + it('skips the very first visit but still counts it', () => { + assert.equal(shouldOfferPwaInstall(), false); + assert.equal(globalThis.localStorage.getItem('pwaVisitCount'), '1'); + }); + + it('offers from the second visit onward', () => { + shouldOfferPwaInstall(); + assert.equal(shouldOfferPwaInstall(), true); + assert.equal(globalThis.localStorage.getItem('pwaVisitCount'), '2'); + }); + + it('stops offering once the popup cap (2) is reached', () => { + globalThis.localStorage.setItem('pwaVisitCount', '5'); + globalThis.localStorage.setItem('pwaPopupCount', '2'); + assert.equal(shouldOfferPwaInstall(), false); + }); + + it('never offers inside an installed PWA window', () => { + globalThis.localStorage.setItem('pwaVisitCount', '5'); + globalThis.__pwaDisplayMode = true; + assert.equal(shouldOfferPwaInstall(), false); + // The visit still counts even when running as a PWA. + assert.equal(globalThis.localStorage.getItem('pwaVisitCount'), '6'); + }); + + it('returns false when storage is unavailable', () => { + globalThis.localStorage._throw = true; + assert.equal(shouldOfferPwaInstall(), false); + }); +}); From 2f9e51390b8dbee53c98d93c920be7345d883e0e Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Thu, 16 Jul 2026 14:01:23 +0800 Subject: [PATCH 24/37] Improvements --- frontend/composables/use-speedtest-charts.js | 8 ++++++-- frontend/data/changelog.json | 10 +++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/composables/use-speedtest-charts.js b/frontend/composables/use-speedtest-charts.js index d59a2fc9a..cc9a62274 100644 --- a/frontend/composables/use-speedtest-charts.js +++ b/frontend/composables/use-speedtest-charts.js @@ -179,8 +179,12 @@ export default function useSpeedTestCharts(t) { // Initialize charts const initCharts = async () => { - // Dynamically import Chart.js - const { Chart, registerables } = await import('chart.js/auto'); + // Dynamically import Chart.js. On broken/blocked visitor networks + // the chunk can fail or resolve null — bail and run chartless + // (downstream already null-guards every charts.* access). + const mod = await import('chart.js/auto').catch(() => null); + if (!mod?.Chart) return; + const { Chart, registerables } = mod; Chart.register(...registerables); const config = getChartConfig(t); diff --git a/frontend/data/changelog.json b/frontend/data/changelog.json index 124b0b572..723e53412 100644 --- a/frontend/data/changelog.json +++ b/frontend/data/changelog.json @@ -1431,10 +1431,10 @@ { "type": "improve", "change": { - "en": "Optimized frontend page cache strategy", - "zh": "优化前端页面的缓存策略", - "fr": "Optimisation de la stratégie de cache des pages frontend", - "tr": "Frontend sayfalarının önbellekleme stratejisi iyileştirildi" + "en": "Optimized loading speed by 50%+", + "zh": "使用多种方式优化加载速度,提升 50%+", + "fr": "Optimisation de la vitesse de chargement de 50%+", + "tr": "Yükleme hızı %50+ artırıldı" } }, { @@ -1457,4 +1457,4 @@ } ] } -] +] \ No newline at end of file From b2eb8806bb7a52951ac5dc61583057c9def9d519 Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Thu, 16 Jul 2026 16:19:30 +0800 Subject: [PATCH 25/37] Improvements --- frontend-server.js | 6 ++++-- frontend/data/changelog.json | 2 +- frontend/main.js | 2 +- frontend/utils/auth-hint.js | 2 +- public/additional/curl.webp | Bin 15454 -> 10054 bytes public/additional/lite.webp | Bin 8812 -> 7560 bytes public/additional/raycast.webp | Bin 16390 -> 9886 bytes tests/auth-hint.test.js | 2 +- 8 files changed, 8 insertions(+), 6 deletions(-) diff --git a/frontend-server.js b/frontend-server.js index dae1a084d..1d6e65e9c 100644 --- a/frontend-server.js +++ b/frontend-server.js @@ -27,7 +27,9 @@ frontendApp.use('/api', createProxyMiddleware({ // Browser TTL on top of these, so the longer values are upper bounds): // - dist/assets/** Vite-hashed JS/CSS/images — content-addressed → 1y immutable // - dist/fonts/** non-hashed but essentially never change → 1y immutable -// - top-level images favicon / logos / achievements / … → 24h +// - top-level images favicon / logos / achievements / … → 7d +// (not content-hashed, so changing one of these +// images requires renaming the file) // - index.html + manifest 24h at the edge (s-maxage), zero in browsers — // the build's postbuild purge evicts them on deploy, // so the TTL only caps drift between deploys; @@ -42,7 +44,7 @@ function setStaticHeaders(res, filePath) { if (rel.startsWith('assets/') || rel.startsWith('fonts/')) { res.setHeader('Cache-Control', `public, max-age=${24 * 60 * 60 * 365}, immutable`); } else if (/\.(png|jpg|jpeg|webp|svg|ico)$/i.test(rel)) { - res.setHeader('Cache-Control', `public, max-age=${24 * 60 * 60}`); + res.setHeader('Cache-Control', `public, max-age=${7 * 24 * 60 * 60}`); } else if (rel.endsWith('.html') || rel === 'manifest.webmanifest') { res.setHeader('Cache-Control', `public, max-age=0, s-maxage=${24 * 60 * 60}, must-revalidate`); } else { diff --git a/frontend/data/changelog.json b/frontend/data/changelog.json index 723e53412..39dffef4e 100644 --- a/frontend/data/changelog.json +++ b/frontend/data/changelog.json @@ -1425,7 +1425,7 @@ ] }, { - "version": "v7.0.1", + "version": "v7.1.0", "date": "Beta", "content": [ { diff --git a/frontend/main.js b/frontend/main.js index b83f9258a..23feb5c70 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -43,7 +43,7 @@ if (import.meta.env.DEV) { // event just propagates as before. Registered at module eval so it's in // place before any lazy import can fail. window.addEventListener('vite:preloadError', (event) => { - const LATCH_KEY = 'jn-preload-reload-at'; + const LATCH_KEY = 'preloadReloadAt'; const lastReload = Number(sessionStorage.getItem(LATCH_KEY) || 0); if (Date.now() - lastReload < 60 * 1000) return; sessionStorage.setItem(LATCH_KEY, String(Date.now())); diff --git a/frontend/utils/auth-hint.js b/frontend/utils/auth-hint.js index 9d4f7e83f..7c6f09fce 100644 --- a/frontend/utils/auth-hint.js +++ b/frontend/utils/auth-hint.js @@ -2,7 +2,7 @@ // decide whether Firebase Auth is worth loading before first render. // '1' = signed in last we knew · '0' = known signed-out · null = unknown // (first visit since the flag shipped, or storage cleared/disabled). -const AUTH_HINT_KEY = 'jn-auth-hint'; +const AUTH_HINT_KEY = 'authHint'; export const readAuthHint = () => { try { diff --git a/public/additional/curl.webp b/public/additional/curl.webp index f04ef450f4b8eaeca72af9681bac6b569928ec7d..165277da73e813cb02da647e1ab327c7945e30e0 100644 GIT binary patch literal 10054 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 diff --git a/public/additional/lite.webp b/public/additional/lite.webp index efad3017b1c821084defa1dd8aa1cc7dcc1fc23e..ef9ee36535f0c67169d5f01e09e25b308103780f 100644 GIT binary patch literal 7560 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 diff --git a/public/additional/raycast.webp b/public/additional/raycast.webp index 17a89676118e3f61ecd78d5cb40106e8c4782fed..bc67189116fe289f4f78f32d5ef226c1f981a180 100644 GIT binary patch literal 9886 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 { }); it('treats unexpected stored values as unknown', () => { - globalThis.localStorage.setItem('jn-auth-hint', 'yes'); + globalThis.localStorage.setItem('authHint', 'yes'); assert.equal(readAuthHint(), null); }); From 72ba1879b032c47f14e16557b0afc4e61915fe19 Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Fri, 17 Jul 2026 00:41:05 +0800 Subject: [PATCH 26/37] Preload the active locale pack during HTML parse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mount waits on the locale messages, but their dynamic import used to start only after index.js had downloaded and executed — one serialized round-trip on the boot critical path. A build-time plugin now injects a head script that picks the language (same order as locales/i18n.js) and adds modulepreload links, so the packs download in parallel with the main bundle. ~460ms faster mount on a simulated slow-4G link. Co-Authored-By: Claude Fable 5 --- vite.config.js | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/vite.config.js b/vite.config.js index eea095789..474194857 100644 --- a/vite.config.js +++ b/vite.config.js @@ -4,6 +4,7 @@ import vue from '@vitejs/plugin-vue' import tailwindcss from '@tailwindcss/vite' import { CodeInspectorPlugin } from 'code-inspector-plugin'; import { sentryVitePlugin } from '@sentry/vite-plugin'; +import { PREFS_STORAGE_KEY, LEGACY_PREFS_KEYS } from './frontend/data/default-preferences.js'; dotenv.config(); @@ -70,6 +71,70 @@ function siteUrlHtmlPlugin() { }; } +// Build-only inline script that modulepreloads the visitor's locale pack. +// Mount waits on the active locale's messages, but their dynamic import can +// only start after index.js has downloaded and executed — a serialized +// round-trip on the boot critical path. This plugin finds the emitted +// locale-pack chunks in the bundle and injects a small head script that +// picks the language exactly like locales/i18n.js (stored prefs incl. +// legacy keys → ?hl= → browser language → en) and appends +// for it (plus the en fallback pack, which +// non-English boots also await) while the HTML is still streaming — the +// packs then download in parallel with the main bundle. A wrong pick only +// wastes one preload; the real import decides. Dev serves no bundle, so +// nothing is injected there. +const localePreloadPlugin = () => { + const preloadScript = (chunks) => `(function () { + var chunks = ${JSON.stringify(chunks)}; + var keys = ${JSON.stringify([PREFS_STORAGE_KEY, ...LEGACY_PREFS_KEYS])}; + var lang = null; + for (var i = 0; i < keys.length && !lang; i++) { + try { + var stored = JSON.parse(localStorage.getItem(keys[i]) || '{}').lang; + if (chunks[stored]) lang = stored; + } catch (e) { /* malformed entry — try the next key */ } + } + if (!lang) { + var hl = new URLSearchParams(location.search).get('hl'); + if (hl) { + lang = chunks[hl] ? hl : 'en'; + } else { + var bl = (navigator.language || '').slice(0, 2).toLowerCase(); + lang = chunks[bl] ? bl : 'en'; + } + } + (lang === 'en' ? ['en'] : [lang, 'en']).forEach(function (l) { + if (!chunks[l]) return; + var link = document.createElement('link'); + link.rel = 'modulepreload'; + link.crossOrigin = ''; + link.href = chunks[l]; + document.head.appendChild(link); + }); +})();`; + + return { + name: 'locale-preload', + transformIndexHtml: { + order: 'post', + handler(html, ctx) { + const chunks = {}; + for (const [fileName, chunk] of Object.entries(ctx.bundle || {})) { + if (chunk.type !== 'chunk') continue; + const facade = (chunk.facadeModuleId || '').replaceAll('\\', '/'); + const match = facade.match(/\/frontend\/locales\/([a-z]{2})\.json$/); + if (match) chunks[match[1]] = '/' + fileName; + } + if (Object.keys(chunks).length === 0) return html; + return { + html, + tags: [{ tag: 'script', children: preloadScript(chunks), injectTo: 'head-prepend' }], + }; + }, + }, + }; +}; + function manualChunks(id) { const normalizedId = id.replaceAll('\\', '/'); @@ -99,6 +164,7 @@ export default defineConfig({ }), tailwindcss(), siteUrlHtmlPlugin(), + localePreloadPlugin(), CodeInspectorPlugin({ bundler: 'vite', hideDomPathAttr: true, From d36e5d639466874152ce915d46b94c337546b7ab Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Fri, 17 Jul 2026 01:01:41 +0800 Subject: [PATCH 27/37] Load Sentry after mount with an early-error buffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SDK chunk used to start downloading at boot, competing with the locale pack the first render waits on. It now loads in the mount chain's finally (same slot as the flag icons). Until init, temporary window listeners buffer uncaught errors/rejections and any of them triggers an immediate load, so a boot that never reaches mount still reports — strictly better coverage than the old always-eager load. Co-Authored-By: Claude Fable 5 --- frontend/main.js | 32 ++++++++++++++++++++++++++------ frontend/sentry-init.js | 15 ++++++++++++++- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/frontend/main.js b/frontend/main.js index 23feb5c70..888803829 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -67,13 +67,31 @@ app.use(i18n); app.use(router); // Sentry — build-time env gate: without the DSN the chunk neither ships nor -// loads. Deliberately not awaited before mount: perf data survives a late -// init (buffered observers, backdated pageload span); only errors thrown -// before init are lost. +// loads. The SDK chunk stays OFF the boot critical path: it loads after +// mount (see the mount chain's finally below) so it never competes with the +// locale pack the first render waits on. Until init, a tiny buffer catches +// uncaught errors / rejections — and any of them triggers an immediate +// load, so a boot that never reaches mount still reports. Perf data +// survives the late init (buffered observers, backdated pageload span). +const earlyErrors = []; +let loadSentry = () => {}; if (import.meta.env.VITE_SENTRY_DSN_FRONTEND) { - import('./sentry-init') - .then(({ initSentry }) => initSentry(app, router)) - .catch(() => {}); + const onEarlyError = (event) => { + earlyErrors.push(event); + loadSentry(); + }; + window.addEventListener('error', onEarlyError); + window.addEventListener('unhandledrejection', onEarlyError); + let loading = null; + loadSentry = () => { + loading ??= import('./sentry-init') + .then(({ initSentry }) => { + initSentry(app, router, earlyErrors); + window.removeEventListener('error', onEarlyError); + window.removeEventListener('unhandledrejection', onEarlyError); + }) + .catch(() => {}); + }; } // @@ -126,10 +144,12 @@ Promise.all([ ]).then(() => { app.mount('#app'); }).catch(error => { + earlyErrors.push(error); // reaches Sentry once it initializes below console.error("Failed to initialize the app properly:", error); app.mount('#app'); // Mount even if initialization partially failed }).finally(() => { loadFlagIcons(); // deferred boot-bandwidth work, app is on screen now + loadSentry(); // Unknown hint (first visit since the flag shipped, or storage cleared): // probe auth once in the background so an already-signed-in user is // recognized and the next boot takes the exact path. diff --git a/frontend/sentry-init.js b/frontend/sentry-init.js index cd1ab7f09..4a5cc8dfc 100644 --- a/frontend/sentry-init.js +++ b/frontend/sentry-init.js @@ -12,7 +12,11 @@ import { isValidIP } from '@/utils/valid-ip.js'; const env = import.meta.env ?? {}; -const initSentry = (app, router) => { +// `earlyErrors` is main.js's pre-init buffer: ErrorEvent / +// PromiseRejectionEvent entries from its temporary window listeners, plus +// the raw Error from the mount chain's catch. Flushed right after init; +// main.js removes those listeners as soon as this function returns. +const initSentry = (app, router, earlyErrors = []) => { Sentry.init({ app, dsn: env.VITE_SENTRY_DSN_FRONTEND, @@ -127,6 +131,15 @@ const initSentry = (app, router) => { }, }); + // Replay the errors that fired before the SDK was ready. Unwrap the + // event shapes first so real Error objects keep their stacks. + for (const entry of earlyErrors) { + const err = entry instanceof Error + ? entry + : entry?.error ?? entry?.reason ?? entry?.message ?? entry; + Sentry.captureException(err); + } + // Explicit product signals arrive via the app-events bus — business code // stays Sentry-free and just emits domain events (see frontend/AGENTS.md). // From 6c0a54bef972b749c236bcbf9fada45c0119ea5c Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Fri, 17 Jul 2026 01:11:11 +0800 Subject: [PATCH 28/37] Improvements --- frontend/composables/use-maxmind.js | 5 ++++- frontend/utils/authenticated-fetch.js | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/composables/use-maxmind.js b/frontend/composables/use-maxmind.js index 076967dc3..7e642c13b 100644 --- a/frontend/composables/use-maxmind.js +++ b/frontend/composables/use-maxmind.js @@ -69,7 +69,10 @@ export function useMaxmind() { const asn = ipData.asn && ipData.asn !== 'N/A' ? ipData.asn : ''; return { country_code, country, org: ipData.isp || '', asn }; } catch (error) { - console.error('useMaxmind lookup failed', error); + // warn, not error: a single-source lookup failure is routine + // visitor-side noise (Sentry only captures console.error); + // a real /api/maxmind outage reports from the backend itself. + console.warn('useMaxmind lookup failed', error); return null; } }); diff --git a/frontend/utils/authenticated-fetch.js b/frontend/utils/authenticated-fetch.js index 2e9f3b07c..9904ec2ba 100644 --- a/frontend/utils/authenticated-fetch.js +++ b/frontend/utils/authenticated-fetch.js @@ -44,6 +44,10 @@ export async function authenticatedFetch(url, method = 'GET', body = null, timeo return response.json(); } catch (error) { + // Timeout aborts keep their identity: rewrapping would hide the + // AbortError name from sentry-init's beforeSend filter, which drops + // them as visitor connectivity noise rather than defects. + if (error.name === 'AbortError') throw error; throw new Error(`Fetch failed: ${error.message}`); } } From e878ddb0e4453039268192c0a96ab690cf7ffc41 Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Fri, 17 Jul 2026 01:19:03 +0800 Subject: [PATCH 29/37] Improvements --- vite.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.js b/vite.config.js index 474194857..8d0e8cdca 100644 --- a/vite.config.js +++ b/vite.config.js @@ -128,7 +128,7 @@ const localePreloadPlugin = () => { if (Object.keys(chunks).length === 0) return html; return { html, - tags: [{ tag: 'script', children: preloadScript(chunks), injectTo: 'head-prepend' }], + tags: [{ tag: 'script', children: preloadScript(chunks), injectTo: 'head' }], }; }, }, From 55286689af4e0c82e062f8adb5dcd61146c93825 Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Fri, 17 Jul 2026 02:10:31 +0800 Subject: [PATCH 30/37] Replace the Turkish locale with Russian GA shows <10 daily Turkish-language visitors after years of a native UI, while ~100 Russian-language visitors show the longest engagement on the site despite seeing English. Swap keeps the maintenance surface at four locales. Stored lang=tr preferences fall back gracefully, and MaxMind natively carries ru geo names (it never had tr ones). Co-Authored-By: Claude Fable 5 --- AGENTS.md | 2 +- README.md | 2 +- README_FR.md | 2 +- README_RU.md | 233 +++ README_TR.md | 232 --- README_ZH.md | 2 +- common/langs.js | 5 +- frontend/AGENTS.md | 2 +- frontend/components/PrivacyPolicy.vue | 2 +- .../advanced-tools/EnhancedDnsLeakTest.vue | 2 +- .../advanced-tools/SecurityChecklist.vue | 2 +- frontend/components/widgets/Preferences.vue | 2 +- frontend/data/changelog.json | 287 +-- frontend/locales/i18n.js | 4 +- frontend/locales/privacy/ru.json | 82 + frontend/locales/privacy/tr.json | 82 - frontend/locales/ru.json | 777 ++++++++ frontend/locales/security-checklist/ru.json | 300 ++++ frontend/locales/security-checklist/tr.json | 1592 ----------------- frontend/locales/tr.json | 963 ---------- index.html | 22 +- tests/changelog.test.js | 2 +- tests/country-name.test.js | 6 +- 23 files changed, 1570 insertions(+), 3035 deletions(-) create mode 100644 README_RU.md delete mode 100644 README_TR.md create mode 100644 frontend/locales/privacy/ru.json delete mode 100644 frontend/locales/privacy/tr.json create mode 100644 frontend/locales/ru.json create mode 100644 frontend/locales/security-checklist/ru.json delete mode 100644 frontend/locales/security-checklist/tr.json delete mode 100644 frontend/locales/tr.json diff --git a/AGENTS.md b/AGENTS.md index 69d01ee5a..02e1cc632 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ halves: a Vue 3 SPA front-end and an Express 5 back-end API. | Layer | Technology | |---|---| -| Frontend | Vue 3 (` @@ -353,15 +353,15 @@ 'On demande au DNS où vous êtes…', "C'est presque prêt, promis…", ], - tr: [ - 'Ağ kartı sürücüsü kuruluyor…', - 'RJ45 konnektörü hizalanıyor…', - 'Fiber kablo modeme takılıyor…', - 'Ethernet portundaki toz üfleniyor…', - 'Modem yeniden başlatılıyor (1. deneme)…', - 'Servis sağlayıcıyla üçlü el sıkışılıyor…', - "DNS'e tam olarak nerede olduğun soruluyor…", - 'Az kaldı, gerçekten…', + ru: [ + 'Устанавливаем драйвер сетевой карты…', + 'Целимся коннектором в порт RJ45…', + 'Подключаем оптоволокно к роутеру…', + 'Выдуваем пыль из Ethernet-порта…', + 'Перезагружаем модем (попытка №1)…', + 'Тройное рукопожатие с провайдером…', + 'Спрашиваем у DNS, где вы на самом деле…', + 'Почти готово, честно…', ], }; @@ -372,7 +372,7 @@ en: 'Waiting too long? Try IPCheck.ing Lite, or refresh the page.', zh: '等得太久?试试 IPCheck.ing Lite,或刷新页面。', fr: 'C’est un peu long ? Essayez IPCheck.ing Lite, ou actualisez la page.', - tr: 'Çok mu uzun sürdü? IPCheck.ing Lite’ı deneyin veya sayfayı yenileyin.', + ru: 'Слишком долго? Попробуйте IPCheck.ing Lite или обновите страницу.', }; const pickLang = () => { diff --git a/tests/changelog.test.js b/tests/changelog.test.js index 1b3af538c..0f0f2c19b 100644 --- a/tests/changelog.test.js +++ b/tests/changelog.test.js @@ -7,7 +7,7 @@ import fs from 'node:fs'; import changelog from '../frontend/data/changelog.json' with { type: 'json' }; -const REQUIRED_LOCALES = ['en', 'zh', 'fr', 'tr']; +const REQUIRED_LOCALES = ['en', 'zh', 'fr', 'ru']; const VALID_TYPES = new Set(['add', 'improve', 'fix']); describe('changelog.json', () => { diff --git a/tests/country-name.test.js b/tests/country-name.test.js index 683591aee..f27670cf7 100644 --- a/tests/country-name.test.js +++ b/tests/country-name.test.js @@ -25,9 +25,9 @@ describe('getCountryName(abbr, lang)', () => { assert.equal(getCountryName('UG', 'fr'), 'Ouganda'); }); - it('returns Turkish name for tr', () => { - assert.equal(getCountryName('TR', 'tr'), 'Türkiye'); - assert.equal(getCountryName('US', 'tr'), 'Amerika Birleşik Devletleri'); + it('returns Russian name for ru', () => { + assert.equal(getCountryName('RU', 'ru'), 'Россия'); + assert.equal(getCountryName('US', 'ru'), 'Соединенные Штаты'); }); it('is case-insensitive on the country code', () => { From 58749aebc0aea4fc01adadf0eeca4fac83d9133e Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Fri, 17 Jul 2026 02:45:04 +0800 Subject: [PATCH 31/37] Polish the Russian locale after proofreading --- frontend/components/Footer.vue | 2 +- frontend/components/Nav.vue | 3 +- frontend/data/changelog.json | 36 +- frontend/locales/en.json | 1 - frontend/locales/fr.json | 1 - frontend/locales/privacy/ru.json | 6 +- frontend/locales/ru.json | 75 ++- frontend/locales/security-checklist/ru.json | 520 ++++++++++---------- frontend/locales/zh.json | 1 - 9 files changed, 320 insertions(+), 325 deletions(-) diff --git a/frontend/components/Footer.vue b/frontend/components/Footer.vue index 35c33ebe4..cd20b5632 100644 --- a/frontend/components/Footer.vue +++ b/frontend/components/Footer.vue @@ -3,7 +3,7 @@ -
+