mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: production CSP blocking PostHog/Sentry/Scalar and silent failure hardening
The production CSP had connect-src/script-src/font-src set to 'self' only, silently blocking all analytics and error reporting in production while working fine in dev (where CSP is not applied). CSP fixes: - Add PostHog ingest + assets origins to connect-src and script-src - Add Sentry ingest origin to connect-src - Add Scalar fonts origin to font-src for API docs pages - Extract CSP construction into testable buildCsp() function Silent failure hardening: - Settings/features stores now set loadError flag and allow retry on subsequent fetch() calls instead of permanently caching failed state - Analytics init no longer sets initialized=true before the try block, allowing retry on failure - Settings dialog Tools section disables save button when settings failed to load, preventing accidental config wipe - Branding logo storage moved from process.cwd() to FILES_STORAGE_PATH so logos persist across Docker container recreation Test coverage: - 16 CSP directive tests covering all external service domains - Store retry-on-error behavior tests for settings and features stores - Analytics init retry-after-failure test
This commit is contained in:
@@ -11,6 +11,7 @@ interface BundleProgress {
|
||||
interface FeaturesState {
|
||||
bundles: FeatureBundleState[];
|
||||
loaded: boolean;
|
||||
loadError: boolean;
|
||||
installing: Record<string, BundleProgress>;
|
||||
errors: Record<string, string>;
|
||||
queued: string[];
|
||||
@@ -149,6 +150,7 @@ export const useFeaturesStore = create<FeaturesState>((set, get) => {
|
||||
return {
|
||||
bundles: [],
|
||||
loaded: false,
|
||||
loadError: false,
|
||||
installing: {},
|
||||
errors: {},
|
||||
queued: [],
|
||||
@@ -156,13 +158,13 @@ export const useFeaturesStore = create<FeaturesState>((set, get) => {
|
||||
startTimes: {},
|
||||
|
||||
fetch: async () => {
|
||||
if (get().loaded) return;
|
||||
if (get().loaded && !get().loadError) return;
|
||||
try {
|
||||
const data = await apiGet<{ bundles: FeatureBundleState[] }>("/v1/features");
|
||||
set({ bundles: data.bundles, loaded: true });
|
||||
set({ bundles: data.bundles, loaded: true, loadError: false });
|
||||
recoverActiveInstalls();
|
||||
} catch {
|
||||
set({ loaded: true });
|
||||
set({ loaded: true, loadError: true });
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ interface SettingsState {
|
||||
defaultToolView: "sidebar" | "fullscreen";
|
||||
defaultTheme: Theme;
|
||||
loaded: boolean;
|
||||
loadError: boolean;
|
||||
fetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -21,9 +22,10 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
||||
defaultToolView: "sidebar",
|
||||
defaultTheme: "light",
|
||||
loaded: false,
|
||||
loadError: false,
|
||||
|
||||
fetch: async () => {
|
||||
if (get().loaded) return;
|
||||
if (get().loaded && !get().loadError) return;
|
||||
try {
|
||||
const data = await apiGet<{
|
||||
settings: Record<string, string>;
|
||||
@@ -39,11 +41,12 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
||||
defaultToolView: data.settings.defaultToolView === "fullscreen" ? "fullscreen" : "sidebar",
|
||||
defaultTheme,
|
||||
loaded: true,
|
||||
loadError: false,
|
||||
});
|
||||
|
||||
useThemeStore.getState().applyServerDefault(defaultTheme);
|
||||
} catch {
|
||||
set({ loaded: true });
|
||||
set({ loaded: true, loadError: true });
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user