Complete stabilization pass bringing all 3 stacks to green: Frontend (apps/web/): - Fix TypeScript nullability in useSeason.ts, useTimeOfDay.ts hooks - Disable no-undef in ESLint config (TypeScript handles it; JSX misidentified) - Rename 306 story imports from @storybook/react to @storybook/react-vite - Fix conditional hook call in useMediaQuery.ts useIsTablet - Move useQuery to top of LoginPage.tsx component - Remove useless try/catch in GearFormModal.tsx - Fix stale closure in ResetPasswordPage.tsx handleChange - Make Storybook decorators (withRouter, withQueryClient, withToast, withAudio) no-ops since global StorybookDecorator already provides these — prevents nested Router / duplicate provider crashes in vitest-browser - Fix nested MemoryRouter in 3 page stories (TrackDetail, PlaylistDetail, UserProfile) - Update i18n initialization in test setup (await init before changeLanguage) - Update ~30 test assertions from English to French to match i18n translations - Update test assertions to match SUMI V3 design changes (shadow vs border) - Fix remaining story type errors (PlayerError, PlaylistBatchActions, TrackFilters, VirtualizedChatMessages) Backend (veza-backend-api/): - Fix response_test.go RespondWithAppError signature (2 args, not 3) - Fix TestErrorContractAuthEndpoints expected error codes (ErrCodeUnauthorized vs ErrCodeInvalidCredentials) - Fix TestTrackHandler_GetTrackLikes_Success missing auth middleware setup - Fix TestPlaybackAnalyticsService_GetTrackStats k-anonymity threshold (needs 5 unique users, not 1) - Replace NOW() PostgreSQL function with time.Now() parameter in marketplace service for SQLite test compatibility - Add missing AutoMigrate entries in marketplace_test.go (ProductImage, ProductPreview, ProductLicense, ProductReview) Results: - Frontend TypeCheck: 617 errors -> 0 errors - Frontend ESLint: 349 errors -> 0 errors - Frontend Vitest: 196 failing tests -> 1 skipped (3396/3397 passing) - Backend go vet: 1 error -> 0 errors - Backend tests: 5 failing -> all 13 packages passing - Rust: 150/150 tests passing (unchanged) - Storybook audit: 0 errors across 1244 stories Triage report: docs/TRIAGE_REPORT.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
158 lines
6 KiB
TypeScript
158 lines
6 KiB
TypeScript
import { z } from 'zod';
|
|
import { logger } from '@/utils/logger';
|
|
|
|
// Schéma de validation pour les variables d'environnement
|
|
// Aligné avec FRONTEND_INTEGRATION.md
|
|
// Support URLs relatives (commençant par /) ou absolues
|
|
const urlOrPathSchema = z.string().refine(
|
|
(val) => {
|
|
if (!val) return false;
|
|
// Accepter les URLs absolues (http://, https://, ws://, wss://)
|
|
if (/^https?:\/\//.test(val) || /^wss?:\/\//.test(val)) {
|
|
try {
|
|
new URL(val);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
// Accepter les chemins relatifs (commençant par /)
|
|
return val.startsWith('/');
|
|
},
|
|
{ message: 'Must be a valid URL or a path starting with /' }
|
|
);
|
|
|
|
// --- Domain (single source of truth for frontend URLs) ---
|
|
// Change VITE_DOMAIN in .env.local to switch domain everywhere.
|
|
const domain = import.meta.env.VITE_DOMAIN || 'veza.fr';
|
|
|
|
const envSchema = z.object({
|
|
VITE_DOMAIN: z.string().default('veza.fr'),
|
|
VITE_API_URL: urlOrPathSchema.default('/api/v1'),
|
|
VITE_WS_URL: urlOrPathSchema.optional(),
|
|
VITE_STREAM_URL: urlOrPathSchema.default(`ws://${domain}:8082/stream`),
|
|
VITE_HLS_BASE_URL: urlOrPathSchema.optional(),
|
|
VITE_UPLOAD_URL: urlOrPathSchema.default('/upload'),
|
|
VITE_APP_NAME: z.string().default('Veza'),
|
|
VITE_API_VERSION: z.string().default('v1'),
|
|
VITE_DEBUG: z
|
|
.string()
|
|
.transform((val) => val === 'true' || val === '1')
|
|
.default('false'),
|
|
VITE_USE_MSW: z
|
|
.string()
|
|
.transform((val) => val === '1' || val === 'true')
|
|
.default('0'),
|
|
VITE_HYPERSWITCH_PUBLISHABLE_KEY: z.string().optional(),
|
|
VITE_FCM_VAPID_KEY: z.string().optional(),
|
|
// FIX #20: Configuration Sentry pour error tracking
|
|
VITE_SENTRY_DSN: z.string().url().optional(),
|
|
});
|
|
|
|
// Validation et parsing des variables d'environnement
|
|
const parseEnv = () => {
|
|
try {
|
|
return envSchema.parse({
|
|
VITE_DOMAIN: import.meta.env.VITE_DOMAIN,
|
|
VITE_API_URL: import.meta.env.VITE_API_URL,
|
|
VITE_WS_URL: import.meta.env.VITE_WS_URL,
|
|
VITE_STREAM_URL: import.meta.env.VITE_STREAM_URL,
|
|
VITE_HLS_BASE_URL: import.meta.env.VITE_HLS_BASE_URL,
|
|
VITE_UPLOAD_URL: import.meta.env.VITE_UPLOAD_URL,
|
|
VITE_APP_NAME: import.meta.env.VITE_APP_NAME,
|
|
VITE_API_VERSION: import.meta.env.VITE_API_VERSION,
|
|
VITE_DEBUG: import.meta.env.VITE_DEBUG,
|
|
VITE_USE_MSW: import.meta.env.VITE_USE_MSW,
|
|
VITE_HYPERSWITCH_PUBLISHABLE_KEY:
|
|
import.meta.env.VITE_HYPERSWITCH_PUBLISHABLE_KEY,
|
|
VITE_FCM_VAPID_KEY: import.meta.env.VITE_FCM_VAPID_KEY,
|
|
VITE_SENTRY_DSN: import.meta.env.VITE_SENTRY_DSN,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
logger.error('❌ Invalid environment variables', {
|
|
errors: error.errors,
|
|
});
|
|
throw new Error(
|
|
`Environment variables validation failed: ${error.errors
|
|
.map((e) => `${e.path.join('.')}: ${e.message}`)
|
|
.join(', ')}`,
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Variables d'environnement validées
|
|
const validatedEnv = parseEnv();
|
|
|
|
// En dev, alerter si l'API est en cross-origin : les cookies ne seront pas envoyés (SameSite),
|
|
// ce qui provoque 401 après login et redirections en boucle. Utiliser VITE_API_URL=/api/v1 (proxy).
|
|
if (import.meta.env.DEV && typeof window !== 'undefined') {
|
|
const apiUrl = validatedEnv.VITE_API_URL;
|
|
if (apiUrl.startsWith('http')) {
|
|
try {
|
|
const apiOrigin = new URL(apiUrl).origin;
|
|
if (window.location.origin !== apiOrigin) {
|
|
logger.warn(
|
|
'[Config] API is cross-origin: cookies will not be sent, login may fail or redirect in a loop. Use VITE_API_URL=/api/v1 so the Vite proxy is used (same origin).',
|
|
{ apiOrigin, pageOrigin: window.location.origin }
|
|
);
|
|
}
|
|
} catch {
|
|
// ignore invalid URL
|
|
}
|
|
}
|
|
}
|
|
|
|
// WS URL: derive from API_URL when not explicitly set
|
|
// v0.502: Chat WS is now at /api/v1/ws on the same backend (no separate Rust server)
|
|
const deriveWSUrl = (): string => {
|
|
if (validatedEnv.VITE_WS_URL) return validatedEnv.VITE_WS_URL;
|
|
const apiUrl = validatedEnv.VITE_API_URL;
|
|
if (apiUrl.startsWith('/')) {
|
|
const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const host = typeof window !== 'undefined' ? window.location.host : 'localhost';
|
|
return `${protocol}//${host}${apiUrl}/ws`;
|
|
}
|
|
const wsUrl = `${apiUrl.replace(/^http/, 'ws') }/ws`;
|
|
return wsUrl;
|
|
};
|
|
|
|
// v0.10.7 F481: Co-listening WebSocket URL (same origin as chat, different path)
|
|
const deriveCoListeningWSUrl = (): string => {
|
|
const wsBase = deriveWSUrl();
|
|
return wsBase.replace(/\/ws$/, '/co-listening/ws');
|
|
};
|
|
|
|
// HLS base URL: explicit or derived from STREAM_URL (ws://host:port -> http://host:port)
|
|
// When STREAM_URL is relative (/stream), use '' so HLS URLs are relative (/hls/...) and get proxied
|
|
const deriveHLSBaseURL = (): string => {
|
|
if (validatedEnv.VITE_HLS_BASE_URL) return validatedEnv.VITE_HLS_BASE_URL;
|
|
const streamUrl = validatedEnv.VITE_STREAM_URL;
|
|
if (streamUrl.startsWith('/')) return ''; // Relative: /hls/... will be proxied
|
|
if (streamUrl.startsWith('ws://')) return streamUrl.replace('ws://', 'http://').replace(/\/stream.*$/, '');
|
|
if (streamUrl.startsWith('wss://')) return streamUrl.replace('wss://', 'https://').replace(/\/stream.*$/, '');
|
|
return `http://${domain}:8082`;
|
|
};
|
|
|
|
// Export de l'objet env avec types
|
|
export const env = {
|
|
DOMAIN: validatedEnv.VITE_DOMAIN,
|
|
API_URL: validatedEnv.VITE_API_URL,
|
|
WS_URL: deriveWSUrl(),
|
|
CO_LISTENING_WS_URL: deriveCoListeningWSUrl(),
|
|
STREAM_URL: validatedEnv.VITE_STREAM_URL,
|
|
HLS_BASE_URL: deriveHLSBaseURL(),
|
|
UPLOAD_URL: validatedEnv.VITE_UPLOAD_URL,
|
|
APP_NAME: validatedEnv.VITE_APP_NAME,
|
|
API_VERSION: validatedEnv.VITE_API_VERSION,
|
|
DEBUG: validatedEnv.VITE_DEBUG,
|
|
USE_MSW: validatedEnv.VITE_USE_MSW,
|
|
FCM_VAPID_KEY: validatedEnv.VITE_FCM_VAPID_KEY,
|
|
// FIX #20: Configuration Sentry
|
|
SENTRY_DSN: validatedEnv.VITE_SENTRY_DSN,
|
|
} as const;
|
|
|
|
// Type pour les variables d'environnement
|
|
export type Env = typeof env;
|