update legal documents and minor code smells
Sonarqube Scanner / Build and analyze (push) Failing after 21s
Sonarqube Scanner / Build and analyze (push) Failing after 21s
This commit is contained in:
+141
-185
@@ -1,11 +1,11 @@
|
||||
globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
|
||||
(function (ns) {
|
||||
'use strict';
|
||||
|
||||
const SETTINGS_KEY = 'wegwichtel.privacy.v1';
|
||||
const PRIVACY_KEY = 'wegwichtel.privacy.v1';
|
||||
const PROGRESS_KEY = 'wegwichtel.route-progress.v1';
|
||||
const COOKIE_PREFIX = 'wegwichtel_';
|
||||
const MAX_PROGRESS_AGE_MS = 24 * 60 * 60 * 1000;
|
||||
const STORE_VERSION = 1;
|
||||
|
||||
function storage() {
|
||||
try {
|
||||
@@ -15,28 +15,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
function now() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
function readJson(key, fallback) {
|
||||
const backend = storage();
|
||||
if (!backend) return fallback;
|
||||
|
||||
const store = storage();
|
||||
if (!store) return fallback;
|
||||
try {
|
||||
const raw = backend.getItem(key);
|
||||
return raw ? JSON.parse(raw) : fallback;
|
||||
const raw = store.getItem(key);
|
||||
if (!raw) return fallback;
|
||||
const value = JSON.parse(raw);
|
||||
return value && typeof value === 'object' ? value : fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function writeJson(key, value) {
|
||||
const backend = storage();
|
||||
if (!backend) return false;
|
||||
|
||||
const store = storage();
|
||||
if (!store) return false;
|
||||
try {
|
||||
backend.setItem(key, JSON.stringify(value));
|
||||
store.setItem(key, JSON.stringify(value));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -44,208 +44,164 @@
|
||||
}
|
||||
|
||||
function removeItem(key) {
|
||||
const backend = storage();
|
||||
if (!backend) return;
|
||||
|
||||
const store = storage();
|
||||
if (!store) return false;
|
||||
try {
|
||||
backend.removeItem(key);
|
||||
store.removeItem(key);
|
||||
return true;
|
||||
} catch {
|
||||
// Private browsing modes may reject storage access. The app remains usable without persistence.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function defaultSettings() {
|
||||
return { progressStorage: false, updatedAt: null };
|
||||
}
|
||||
|
||||
function settings() {
|
||||
const value = readJson(SETTINGS_KEY, defaultSettings());
|
||||
function normalizeSettings(value) {
|
||||
return {
|
||||
progressStorage: value?.progressStorage === true,
|
||||
updatedAt: typeof value?.updatedAt === 'string' ? value.updatedAt : null
|
||||
cookies: false,
|
||||
updatedAt: Number(value?.updatedAt) || now()
|
||||
};
|
||||
}
|
||||
|
||||
function saveSettings(progressStorage) {
|
||||
return writeJson(SETTINGS_KEY, {
|
||||
progressStorage: progressStorage === true,
|
||||
updatedAt: nowIso()
|
||||
});
|
||||
function readSettings() {
|
||||
return normalizeSettings(readJson(PRIVACY_KEY, {}));
|
||||
}
|
||||
|
||||
function defaultProgressStore() {
|
||||
return { version: STORE_VERSION, currentRouteId: null, routes: {} };
|
||||
function writeSettings(settings) {
|
||||
return writeJson(PRIVACY_KEY, normalizeSettings({ ...settings, updatedAt: now() }));
|
||||
}
|
||||
|
||||
function progressStore() {
|
||||
const value = readJson(PROGRESS_KEY, defaultProgressStore());
|
||||
if (value?.version !== STORE_VERSION || typeof value?.routes !== 'object' || value.routes === null) {
|
||||
return defaultProgressStore();
|
||||
}
|
||||
|
||||
return {
|
||||
version: STORE_VERSION,
|
||||
currentRouteId: value.currentRouteId ?? null,
|
||||
routes: value.routes
|
||||
};
|
||||
}
|
||||
|
||||
function persistProgressStore(value) {
|
||||
if (!settings().progressStorage) return false;
|
||||
return writeJson(PROGRESS_KEY, value);
|
||||
}
|
||||
|
||||
function uniqueIds(values) {
|
||||
return [...new Set((Array.isArray(values) ? values : [])
|
||||
.map(value => Number(value))
|
||||
.filter(Number.isFinite))];
|
||||
}
|
||||
|
||||
function routeRecord(route, previous = {}) {
|
||||
const timestamp = nowIso();
|
||||
return {
|
||||
routeId: route.id,
|
||||
routeName: String(route.name ?? previous.routeName ?? ''),
|
||||
routeState: previous.routeState ?? 'idle',
|
||||
routeProgressIndex: Number.isInteger(previous.routeProgressIndex)
|
||||
? previous.routeProgressIndex
|
||||
: null,
|
||||
visitedPoiIds: uniqueIds(previous.visitedPoiIds),
|
||||
viewedPoiIds: uniqueIds(previous.viewedPoiIds),
|
||||
playedAudioPoiIds: uniqueIds(previous.playedAudioPoiIds),
|
||||
startedAt: previous.startedAt ?? timestamp,
|
||||
updatedAt: timestamp
|
||||
};
|
||||
}
|
||||
|
||||
function updateRoute(routeId, updater) {
|
||||
if (!settings().progressStorage || routeId == null) return null;
|
||||
|
||||
const store = progressStore();
|
||||
const key = String(routeId);
|
||||
const current = store.routes[key];
|
||||
if (!current) return null;
|
||||
|
||||
const updated = updater({ ...current });
|
||||
updated.updatedAt = nowIso();
|
||||
store.currentRouteId = routeId;
|
||||
store.routes[key] = updated;
|
||||
persistProgressStore(store);
|
||||
return updated;
|
||||
}
|
||||
|
||||
function markPoi(routeId, field, poiId) {
|
||||
const normalizedId = Number(poiId);
|
||||
if (!Number.isFinite(normalizedId)) return null;
|
||||
|
||||
return updateRoute(routeId, record => {
|
||||
record[field] = uniqueIds([...(record[field] ?? []), normalizedId]);
|
||||
return record;
|
||||
});
|
||||
}
|
||||
|
||||
function clearApplicationCookies() {
|
||||
const cookieText = globalThis.document?.cookie;
|
||||
if (!cookieText) return;
|
||||
|
||||
cookieText.split(';').forEach(item => {
|
||||
const name = item.split('=', 1)[0].trim();
|
||||
if (!name.startsWith(COOKIE_PREFIX)) return;
|
||||
globalThis.document.cookie = `${name}=; Max-Age=0; Path=/; SameSite=Lax`;
|
||||
});
|
||||
function isProgressStorageEnabled() {
|
||||
return readSettings().progressStorage === true;
|
||||
}
|
||||
|
||||
function setProgressStorageEnabled(enabled) {
|
||||
const active = enabled === true;
|
||||
const saved = saveSettings(active);
|
||||
if (!active) removeItem(PROGRESS_KEY);
|
||||
return saved && settings().progressStorage;
|
||||
const nextValue = Boolean(enabled);
|
||||
writeSettings({ progressStorage: nextValue, cookies: false });
|
||||
if (!nextValue) removeItem(PROGRESS_KEY);
|
||||
return nextValue;
|
||||
}
|
||||
|
||||
function cookiesEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function routeId(route) {
|
||||
const id = route?.id ?? route?.routeId ?? route;
|
||||
return id === null || id === undefined ? null : String(id);
|
||||
}
|
||||
|
||||
function asNumberOrNull(value) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number : null;
|
||||
}
|
||||
|
||||
function numericIdSet(values) {
|
||||
if (!Array.isArray(values)) return [];
|
||||
return [...new Set(values.map(Number).filter(Number.isFinite))];
|
||||
}
|
||||
|
||||
function readProgressStore() {
|
||||
const data = readJson(PROGRESS_KEY, {});
|
||||
const entries = data?.routes && typeof data.routes === 'object' ? data.routes : data;
|
||||
return entries && typeof entries === 'object' ? entries : {};
|
||||
}
|
||||
|
||||
function writeProgressStore(routes) {
|
||||
return writeJson(PROGRESS_KEY, {
|
||||
version: 1,
|
||||
updatedAt: now(),
|
||||
routes
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeSavedRouteState(value) {
|
||||
if (!value || typeof value !== 'object') return null;
|
||||
return {
|
||||
routeId: value.routeId === null || value.routeId === undefined ? null : String(value.routeId),
|
||||
routeState: value.routeState === 'running' || value.routeState === 'paused' ? value.routeState : 'idle',
|
||||
routeProgressIndex: Number.isInteger(value.routeProgressIndex) ? value.routeProgressIndex : null,
|
||||
visitedPoiIds: numericIdSet(value.visitedPoiIds),
|
||||
viewedPoiIds: numericIdSet(value.viewedPoiIds),
|
||||
playedAudioPoiIds: numericIdSet(value.playedAudioPoiIds),
|
||||
savedAt: Number(value.savedAt) || Number(value.updatedAt) || 0,
|
||||
updatedAt: Number(value.updatedAt) || Number(value.savedAt) || 0
|
||||
};
|
||||
}
|
||||
|
||||
function isExpired(value, referenceTime = now()) {
|
||||
const saved = Number(value?.updatedAt) || Number(value?.savedAt) || 0;
|
||||
return saved <= 0 || referenceTime - saved > MAX_PROGRESS_AGE_MS;
|
||||
}
|
||||
|
||||
function saveRouteState(route, routeState, routeProgressIndex, visitedPoiIds = [], viewedPoiIds = [], playedAudioPoiIds = []) {
|
||||
if (!isProgressStorageEnabled()) return false;
|
||||
const id = routeId(route);
|
||||
if (!id) return false;
|
||||
const routes = readProgressStore();
|
||||
const timestamp = now();
|
||||
routes[id] = {
|
||||
routeId: id,
|
||||
routeName: route?.name ?? '',
|
||||
routeState: routeState === 'running' || routeState === 'paused' ? routeState : 'idle',
|
||||
routeProgressIndex: Number.isInteger(routeProgressIndex) ? routeProgressIndex : null,
|
||||
visitedPoiIds: numericIdSet(visitedPoiIds),
|
||||
viewedPoiIds: numericIdSet(viewedPoiIds),
|
||||
playedAudioPoiIds: numericIdSet(playedAudioPoiIds),
|
||||
savedAt: timestamp,
|
||||
updatedAt: timestamp
|
||||
};
|
||||
return writeProgressStore(routes);
|
||||
}
|
||||
|
||||
function activateRoute(route) {
|
||||
if (!settings().progressStorage || route?.id == null) return null;
|
||||
|
||||
const store = progressStore();
|
||||
const key = String(route.id);
|
||||
const record = routeRecord(route, store.routes[key]);
|
||||
store.currentRouteId = route.id;
|
||||
store.routes[key] = record;
|
||||
persistProgressStore(store);
|
||||
return record;
|
||||
if (!isProgressStorageEnabled()) return null;
|
||||
const id = routeId(route);
|
||||
if (!id) return null;
|
||||
const routes = cleanupExpiredEntries().routes;
|
||||
const saved = normalizeSavedRouteState(routes[id]);
|
||||
return saved && !isExpired(saved) ? saved : null;
|
||||
}
|
||||
|
||||
function saveRouteState(route, routeState, routeProgressIndex, visitedPoiIds, viewedPoiIds, playedAudioPoiIds) {
|
||||
if (route?.id == null) return null;
|
||||
|
||||
return updateRoute(route.id, record => ({
|
||||
...record,
|
||||
routeName: String(route.name ?? record.routeName ?? ''),
|
||||
routeState: String(routeState ?? 'idle'),
|
||||
routeProgressIndex: Number.isInteger(routeProgressIndex) ? routeProgressIndex : null,
|
||||
visitedPoiIds: uniqueIds(visitedPoiIds),
|
||||
viewedPoiIds: uniqueIds(viewedPoiIds),
|
||||
playedAudioPoiIds: uniqueIds(playedAudioPoiIds)
|
||||
}));
|
||||
}
|
||||
|
||||
function loadRoute(routeId) {
|
||||
if (!settings().progressStorage || routeId == null) return null;
|
||||
return progressStore().routes[String(routeId)] ?? null;
|
||||
}
|
||||
|
||||
function nextTurn() {
|
||||
return new Promise(resolve => globalThis.setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
async function cleanupExpired(onProgress = () => {}) {
|
||||
if (!settings().progressStorage) {
|
||||
removeItem(PROGRESS_KEY);
|
||||
return { removed: 0, total: 0 };
|
||||
}
|
||||
|
||||
const store = progressStore();
|
||||
const cutoff = Date.now() - MAX_PROGRESS_AGE_MS;
|
||||
const expiredKeys = Object.entries(store.routes)
|
||||
.filter(([, route]) => {
|
||||
const timestamp = Date.parse(route?.updatedAt ?? '');
|
||||
return !Number.isFinite(timestamp) || timestamp <= cutoff;
|
||||
})
|
||||
.map(([key]) => key);
|
||||
|
||||
if (!expiredKeys.length) return { removed: 0, total: 0 };
|
||||
|
||||
function cleanupExpiredEntries(progressCallback) {
|
||||
const referenceTime = now();
|
||||
const routes = readProgressStore();
|
||||
const entries = Object.entries(routes);
|
||||
const kept = {};
|
||||
let removed = 0;
|
||||
onProgress(removed, expiredKeys.length);
|
||||
|
||||
for (const key of expiredKeys) {
|
||||
delete store.routes[key];
|
||||
if (String(store.currentRouteId) === key) store.currentRouteId = null;
|
||||
removed += 1;
|
||||
persistProgressStore(store);
|
||||
onProgress(removed, expiredKeys.length);
|
||||
await nextTurn();
|
||||
}
|
||||
entries.forEach(([id, value], index) => {
|
||||
const normalized = normalizeSavedRouteState(value);
|
||||
if (normalized && !isExpired(normalized, referenceTime)) kept[id] = normalized;
|
||||
else removed += 1;
|
||||
reportCleanupProgress(progressCallback, index + 1, entries.length, removed);
|
||||
});
|
||||
|
||||
return { removed, total: expiredKeys.length };
|
||||
if (removed > 0) writeProgressStore(kept);
|
||||
return { removed, total: entries.length, routes: kept };
|
||||
}
|
||||
|
||||
clearApplicationCookies();
|
||||
function reportCleanupProgress(callback, processed, total, removed) {
|
||||
if (typeof callback !== 'function') return;
|
||||
callback({ processed, total, removed });
|
||||
}
|
||||
|
||||
async function cleanupExpiredProgress(progressCallback) {
|
||||
return cleanupExpiredEntries(progressCallback);
|
||||
}
|
||||
|
||||
ns.PrivacyStorage = Object.freeze({
|
||||
SETTINGS_KEY,
|
||||
PRIVACY_KEY,
|
||||
PROGRESS_KEY,
|
||||
MAX_PROGRESS_AGE_MS,
|
||||
cookiesEnabled: () => false,
|
||||
clearApplicationCookies,
|
||||
isProgressStorageEnabled: () => settings().progressStorage,
|
||||
setProgressStorageEnabled,
|
||||
cleanupExpired,
|
||||
activateRoute,
|
||||
loadRoute,
|
||||
cleanExpiredProgress: cleanupExpiredProgress,
|
||||
cleanupExpiredProgress,
|
||||
cookiesEnabled: () => false,
|
||||
getPrivacySettings: readSettings,
|
||||
isProgressStorageEnabled,
|
||||
removeExpiredProgress: cleanupExpiredProgress,
|
||||
removeExpiredRouteProgress: cleanupExpiredProgress,
|
||||
saveRouteState,
|
||||
markVisited: (routeId, poiId) => markPoi(routeId, 'visitedPoiIds', poiId),
|
||||
markViewed: (routeId, poiId) => markPoi(routeId, 'viewedPoiIds', poiId),
|
||||
markAudioPlayed: (routeId, poiId) => markPoi(routeId, 'playedAudioPoiIds', poiId),
|
||||
clearProgress: () => removeItem(PROGRESS_KEY)
|
||||
setProgressStorageEnabled
|
||||
});
|
||||
}(globalThis.Wegwichtel ??= {}));
|
||||
}(globalThis.Wegwichtel));
|
||||
|
||||
Reference in New Issue
Block a user