ソースを参照

Fix frontend API calls missing auth tokens

Several frontend components were using raw fetch() instead of the API
client, causing 401 Unauthorized errors when authentication is enabled.

Changes:
- SpoolmanSettings: Use api.getSpoolmanSettings/updateSpoolmanSettings
- ProjectsPage: Use api.importProjectFile for ZIP imports
- ProjectDetailPage: Use api.exportProjectZip for exports
- CameraPage/EmbeddedCameraViewer: Use api.getCameraStatus

Added to API client:
- getSpoolmanSettings()
- updateSpoolmanSettings()
- importProjectFile()
- exportProjectZip()
- getCameraStatus()

Added tests for auth token handling in client.test.ts
Fixed VirtualPrinterSettings test expectations to match i18n strings

Closes #231
maziggy 7 ヶ月 前
コミット
158fa88b7a

+ 178 - 0
frontend/src/__tests__/api/client.test.ts

@@ -0,0 +1,178 @@
+/**
+ * Tests for the API client auth token handling.
+ */
+
+import { describe, it, expect, afterEach, vi } from 'vitest';
+import { http, HttpResponse } from 'msw';
+import { setupServer } from 'msw/node';
+import { setAuthToken, getAuthToken, api } from '../../api/client';
+
+// Mock localStorage
+const localStorageMock = {
+  store: {} as Record<string, string>,
+  getItem: vi.fn((key: string) => localStorageMock.store[key] || null),
+  setItem: vi.fn((key: string, value: string) => {
+    localStorageMock.store[key] = value;
+  }),
+  removeItem: vi.fn((key: string) => {
+    delete localStorageMock.store[key];
+  }),
+  clear: vi.fn(() => {
+    localStorageMock.store = {};
+  }),
+};
+
+Object.defineProperty(window, 'localStorage', {
+  value: localStorageMock,
+});
+
+// Create MSW server
+const server = setupServer();
+
+beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }));
+afterEach(() => {
+  server.resetHandlers();
+  localStorageMock.clear();
+  setAuthToken(null);
+});
+afterAll(() => server.close());
+
+describe('Auth Token Management', () => {
+  it('setAuthToken stores token in localStorage', () => {
+    setAuthToken('test-token-123');
+    expect(localStorageMock.setItem).toHaveBeenCalledWith('auth_token', 'test-token-123');
+    expect(getAuthToken()).toBe('test-token-123');
+  });
+
+  it('setAuthToken removes token from localStorage when null', () => {
+    setAuthToken('test-token-123');
+    setAuthToken(null);
+    expect(localStorageMock.removeItem).toHaveBeenCalledWith('auth_token');
+    expect(getAuthToken()).toBeNull();
+  });
+});
+
+describe('API Client Auth Header', () => {
+  it('includes Authorization header when token is set', async () => {
+    let capturedHeaders: Headers | null = null;
+
+    server.use(
+      http.get('/api/v1/settings/spoolman', ({ request }) => {
+        capturedHeaders = request.headers;
+        return HttpResponse.json({
+          spoolman_enabled: 'false',
+          spoolman_url: '',
+          spoolman_sync_mode: 'auto',
+        });
+      })
+    );
+
+    setAuthToken('test-jwt-token');
+    await api.getSpoolmanSettings();
+
+    expect(capturedHeaders).not.toBeNull();
+    expect(capturedHeaders!.get('Authorization')).toBe('Bearer test-jwt-token');
+  });
+
+  it('does not include Authorization header when token is not set', async () => {
+    let capturedHeaders: Headers | null = null;
+
+    server.use(
+      http.get('/api/v1/settings/spoolman', ({ request }) => {
+        capturedHeaders = request.headers;
+        return HttpResponse.json({
+          spoolman_enabled: 'false',
+          spoolman_url: '',
+          spoolman_sync_mode: 'auto',
+        });
+      })
+    );
+
+    setAuthToken(null);
+    await api.getSpoolmanSettings();
+
+    expect(capturedHeaders).not.toBeNull();
+    expect(capturedHeaders!.get('Authorization')).toBeNull();
+  });
+
+  it('clears token on 401 Unauthorized response', async () => {
+    server.use(
+      http.get('/api/v1/settings/spoolman', () => {
+        return HttpResponse.json(
+          { detail: 'Not authenticated' },
+          { status: 401 }
+        );
+      })
+    );
+
+    setAuthToken('expired-token');
+    expect(getAuthToken()).toBe('expired-token');
+
+    try {
+      await api.getSpoolmanSettings();
+    } catch {
+      // Expected to throw
+    }
+
+    expect(getAuthToken()).toBeNull();
+    expect(localStorageMock.removeItem).toHaveBeenCalledWith('auth_token');
+  });
+});
+
+describe('FormData requests include auth header', () => {
+  it('importProjectFile includes Authorization header', async () => {
+    let capturedHeaders: Headers | null = null;
+
+    server.use(
+      http.post('/api/v1/projects/import/file', ({ request }) => {
+        capturedHeaders = request.headers;
+        return HttpResponse.json({
+          id: 1,
+          name: 'Test Project',
+          description: '',
+          total_cost: 0,
+          total_print_time_seconds: 0,
+          total_prints: 0,
+          total_quantity: 0,
+          status: 'active',
+          due_date: null,
+          created_at: '2026-01-01T00:00:00Z',
+          updated_at: '2026-01-01T00:00:00Z',
+          archives: [],
+          bom_items: [],
+        });
+      })
+    );
+
+    setAuthToken('test-token');
+    const file = new File(['test content'], 'test.zip', { type: 'application/zip' });
+    await api.importProjectFile(file);
+
+    expect(capturedHeaders).not.toBeNull();
+    expect(capturedHeaders!.get('Authorization')).toBe('Bearer test-token');
+  });
+
+  it('exportProjectZip includes Authorization header', async () => {
+    let capturedHeaders: Headers | null = null;
+
+    server.use(
+      http.get('/api/v1/projects/:projectId/export', ({ request }) => {
+        capturedHeaders = request.headers;
+        const zipContent = new Uint8Array([0x50, 0x4b, 0x03, 0x04]); // ZIP magic bytes
+        return new HttpResponse(zipContent, {
+          status: 200,
+          headers: {
+            'Content-Type': 'application/zip',
+            'Content-Disposition': 'attachment; filename="project.zip"',
+          },
+        });
+      })
+    );
+
+    setAuthToken('test-token');
+    await api.exportProjectZip(1);
+
+    expect(capturedHeaders).not.toBeNull();
+    expect(capturedHeaders!.get('Authorization')).toBe('Bearer test-token');
+  });
+});

+ 24 - 43
frontend/src/__tests__/components/SpoolmanSettings.test.tsx

@@ -18,6 +18,8 @@ vi.mock('../../api/client', () => ({
   api: {
   api: {
     getSettings: vi.fn().mockResolvedValue({}),
     getSettings: vi.fn().mockResolvedValue({}),
     updateSettings: vi.fn().mockResolvedValue({}),
     updateSettings: vi.fn().mockResolvedValue({}),
+    getSpoolmanSettings: vi.fn(),
+    updateSpoolmanSettings: vi.fn(),
     getSpoolmanStatus: vi.fn(),
     getSpoolmanStatus: vi.fn(),
     connectSpoolman: vi.fn(),
     connectSpoolman: vi.fn(),
     disconnectSpoolman: vi.fn(),
     disconnectSpoolman: vi.fn(),
@@ -30,17 +32,21 @@ vi.mock('../../api/client', () => ({
 // Import mocked module
 // Import mocked module
 import { api } from '../../api/client';
 import { api } from '../../api/client';
 
 
-// Mock fetch for Spoolman settings endpoints
-const mockFetchResponse = (data: object) => ({
-  ok: true,
-  json: () => Promise.resolve(data),
-});
-
 describe('SpoolmanSettings', () => {
 describe('SpoolmanSettings', () => {
   beforeEach(() => {
   beforeEach(() => {
     vi.clearAllMocks();
     vi.clearAllMocks();
 
 
     // Default API mocks
     // Default API mocks
+    vi.mocked(api.getSpoolmanSettings).mockResolvedValue({
+      spoolman_enabled: 'false',
+      spoolman_url: '',
+      spoolman_sync_mode: 'auto',
+    });
+    vi.mocked(api.updateSpoolmanSettings).mockResolvedValue({
+      spoolman_enabled: 'false',
+      spoolman_url: '',
+      spoolman_sync_mode: 'auto',
+    });
     vi.mocked(api.getSpoolmanStatus).mockResolvedValue({
     vi.mocked(api.getSpoolmanStatus).mockResolvedValue({
       enabled: false,
       enabled: false,
       connected: false,
       connected: false,
@@ -56,26 +62,12 @@ describe('SpoolmanSettings', () => {
       skipped: [],
       skipped: [],
       errors: [],
       errors: [],
     });
     });
-
-    // Default fetch mock for settings - disabled state
-    global.fetch = vi.fn().mockImplementation((url: string) => {
-      if (url.includes('/api/v1/settings/spoolman')) {
-        return Promise.resolve(
-          mockFetchResponse({
-            spoolman_enabled: 'false',
-            spoolman_url: '',
-            spoolman_sync_mode: 'auto',
-          })
-        );
-      }
-      return Promise.reject(new Error('Unknown URL'));
-    }) as any;
   });
   });
 
 
   describe('rendering', () => {
   describe('rendering', () => {
     it('renders loading state initially', () => {
     it('renders loading state initially', () => {
-      // Delay the fetch response to catch loading state
-      global.fetch = vi.fn().mockImplementation(() => new Promise(() => {})) as any;
+      // Delay the API response to catch loading state
+      vi.mocked(api.getSpoolmanSettings).mockImplementation(() => new Promise(() => {}));
       render(<SpoolmanSettings />);
       render(<SpoolmanSettings />);
 
 
       // Should show loading spinner
       // Should show loading spinner
@@ -159,27 +151,16 @@ describe('SpoolmanSettings', () => {
 
 
   describe('enabled state', () => {
   describe('enabled state', () => {
     beforeEach(() => {
     beforeEach(() => {
-      global.fetch = vi.fn().mockImplementation((url: string) => {
-        if (url.includes('/api/v1/settings/spoolman')) {
-          if (url.includes('PUT') || (url as any).method === 'PUT') {
-            return Promise.resolve(
-              mockFetchResponse({
-                spoolman_enabled: 'true',
-                spoolman_url: 'http://localhost:7912',
-                spoolman_sync_mode: 'auto',
-              })
-            );
-          }
-          return Promise.resolve(
-            mockFetchResponse({
-              spoolman_enabled: 'true',
-              spoolman_url: 'http://localhost:7912',
-              spoolman_sync_mode: 'auto',
-            })
-          );
-        }
-        return Promise.reject(new Error('Unknown URL'));
-      }) as any;
+      vi.mocked(api.getSpoolmanSettings).mockResolvedValue({
+        spoolman_enabled: 'true',
+        spoolman_url: 'http://localhost:7912',
+        spoolman_sync_mode: 'auto',
+      });
+      vi.mocked(api.updateSpoolmanSettings).mockResolvedValue({
+        spoolman_enabled: 'true',
+        spoolman_url: 'http://localhost:7912',
+        spoolman_sync_mode: 'auto',
+      });
     });
     });
 
 
     it('URL input is enabled when Spoolman is enabled', async () => {
     it('URL input is enabled when Spoolman is enabled', async () => {

+ 2 - 2
frontend/src/__tests__/components/VirtualPrinterSettings.test.tsx

@@ -274,7 +274,7 @@ describe('VirtualPrinterSettings', () => {
 
 
       await waitFor(() => {
       await waitFor(() => {
         expect(screen.getByText('Review')).toBeInTheDocument();
         expect(screen.getByText('Review')).toBeInTheDocument();
-        expect(screen.getByText('Review and tag before archiving')).toBeInTheDocument();
+        expect(screen.getByText('Review before archiving')).toBeInTheDocument();
       });
       });
     });
     });
 
 
@@ -283,7 +283,7 @@ describe('VirtualPrinterSettings', () => {
 
 
       await waitFor(() => {
       await waitFor(() => {
         expect(screen.getByText('Queue')).toBeInTheDocument();
         expect(screen.getByText('Queue')).toBeInTheDocument();
-        expect(screen.getByText('Archive and add to print queue')).toBeInTheDocument();
+        expect(screen.getByText('Archive and add to queue')).toBeInTheDocument();
       });
       });
     });
     });
 
 

+ 45 - 0
frontend/src/api/client.ts

@@ -2969,6 +2969,13 @@ export const api = {
       method: 'POST',
       method: 'POST',
       body: JSON.stringify({ tray_uuid: trayUuid }),
       body: JSON.stringify({ tray_uuid: trayUuid }),
     }),
     }),
+  getSpoolmanSettings: () =>
+    request<{ spoolman_enabled: string; spoolman_url: string; spoolman_sync_mode: string }>('/settings/spoolman'),
+  updateSpoolmanSettings: (data: { spoolman_enabled?: string; spoolman_url?: string; spoolman_sync_mode?: string }) =>
+    request<{ spoolman_enabled: string; spoolman_url: string; spoolman_sync_mode: string }>('/settings/spoolman', {
+      method: 'PUT',
+      body: JSON.stringify(data),
+    }),
 
 
   // Updates
   // Updates
   getVersion: () => request<VersionInfo>('/updates/version'),
   getVersion: () => request<VersionInfo>('/updates/version'),
@@ -3030,6 +3037,8 @@ export const api = {
     `${API_BASE}/printers/${printerId}/camera/snapshot`,
     `${API_BASE}/printers/${printerId}/camera/snapshot`,
   testCameraConnection: (printerId: number) =>
   testCameraConnection: (printerId: number) =>
     request<{ success: boolean; message?: string; error?: string }>(`/printers/${printerId}/camera/test`),
     request<{ success: boolean; message?: string; error?: string }>(`/printers/${printerId}/camera/test`),
+  getCameraStatus: (printerId: number) =>
+    request<{ active: boolean; stalled: boolean }>(`/printers/${printerId}/camera/status`),
 
 
   // Plate Detection - Multi-reference calibration (stores up to 5 references per printer)
   // Plate Detection - Multi-reference calibration (stores up to 5 references per printer)
   checkPlateEmpty: (printerId: number, options?: { useExternal?: boolean; includeDebugImage?: boolean }) => {
   checkPlateEmpty: (printerId: number, options?: { useExternal?: boolean; includeDebugImage?: boolean }) => {
@@ -3234,6 +3243,42 @@ export const api = {
       method: 'POST',
       method: 'POST',
       body: JSON.stringify(data),
       body: JSON.stringify(data),
     }),
     }),
+  importProjectFile: async (file: File): Promise<Project> => {
+    const formData = new FormData();
+    formData.append('file', file);
+    const headers: Record<string, string> = {};
+    if (authToken) {
+      headers['Authorization'] = `Bearer ${authToken}`;
+    }
+    const response = await fetch(`${API_BASE}/projects/import/file`, {
+      method: 'POST',
+      headers,
+      body: formData,
+    });
+    if (!response.ok) {
+      const error = await response.json().catch(() => ({}));
+      throw new Error(error.detail || `HTTP ${response.status}`);
+    }
+    return response.json();
+  },
+  exportProjectZip: async (projectId: number): Promise<{ blob: Blob; filename: string }> => {
+    const headers: Record<string, string> = {};
+    if (authToken) {
+      headers['Authorization'] = `Bearer ${authToken}`;
+    }
+    const response = await fetch(`${API_BASE}/projects/${projectId}/export`, {
+      headers,
+    });
+    if (!response.ok) {
+      const error = await response.json().catch(() => ({}));
+      throw new Error(error.detail || `HTTP ${response.status}`);
+    }
+    const contentDisposition = response.headers.get('Content-Disposition');
+    const filenameMatch = contentDisposition?.match(/filename="(.+)"/);
+    const filename = filenameMatch?.[1] || `project_${projectId}.zip`;
+    const blob = await response.blob();
+    return { blob, filename };
+  },
 
 
   // API Keys
   // API Keys
   getAPIKeys: () => request<APIKey[]>('/api-keys/'),
   getAPIKeys: () => request<APIKey[]>('/api-keys/'),

+ 7 - 10
frontend/src/components/EmbeddedCameraViewer.tsx

@@ -184,17 +184,14 @@ export function EmbeddedCameraViewer({ printerId, printerName, viewerIndex = 0,
 
 
     stallCheckIntervalRef.current = setInterval(async () => {
     stallCheckIntervalRef.current = setInterval(async () => {
       try {
       try {
-        const response = await fetch(`/api/v1/printers/${printerId}/camera/status`);
-        if (response.ok) {
-          const status = await response.json();
-          if (status.stalled || (!status.active && !streamError)) {
-            if (stallCheckIntervalRef.current) {
-              clearInterval(stallCheckIntervalRef.current);
-              stallCheckIntervalRef.current = null;
-            }
-            setStreamLoading(false);
-            attemptReconnect();
+        const status = await api.getCameraStatus(printerId);
+        if (status.stalled || (!status.active && !streamError)) {
+          if (stallCheckIntervalRef.current) {
+            clearInterval(stallCheckIntervalRef.current);
+            stallCheckIntervalRef.current = null;
           }
           }
+          setStreamLoading(false);
+          attemptReconnect();
         }
         }
       } catch {
       } catch {
         // Ignore errors
         // Ignore errors

+ 2 - 28
frontend/src/components/SpoolmanSettings.tsx

@@ -6,32 +6,6 @@ import type { SpoolmanSyncResult, Printer } from '../api/client';
 import { Card, CardContent, CardHeader } from './Card';
 import { Card, CardContent, CardHeader } from './Card';
 import { Button } from './Button';
 import { Button } from './Button';
 
 
-interface SpoolmanSettingsData {
-  spoolman_enabled: string;
-  spoolman_url: string;
-  spoolman_sync_mode: string;
-}
-
-async function getSpoolmanSettings(): Promise<SpoolmanSettingsData> {
-  const response = await fetch('/api/v1/settings/spoolman');
-  if (!response.ok) {
-    throw new Error('Failed to load Spoolman settings');
-  }
-  return response.json();
-}
-
-async function updateSpoolmanSettings(data: Partial<SpoolmanSettingsData>): Promise<SpoolmanSettingsData> {
-  const response = await fetch('/api/v1/settings/spoolman', {
-    method: 'PUT',
-    headers: { 'Content-Type': 'application/json' },
-    body: JSON.stringify(data),
-  });
-  if (!response.ok) {
-    throw new Error('Failed to save Spoolman settings');
-  }
-  return response.json();
-}
-
 export function SpoolmanSettings() {
 export function SpoolmanSettings() {
   const queryClient = useQueryClient();
   const queryClient = useQueryClient();
   const [localEnabled, setLocalEnabled] = useState(false);
   const [localEnabled, setLocalEnabled] = useState(false);
@@ -45,7 +19,7 @@ export function SpoolmanSettings() {
   // Fetch Spoolman settings
   // Fetch Spoolman settings
   const { data: settings, isLoading: settingsLoading } = useQuery({
   const { data: settings, isLoading: settingsLoading } = useQuery({
     queryKey: ['spoolman-settings'],
     queryKey: ['spoolman-settings'],
-    queryFn: getSpoolmanSettings,
+    queryFn: api.getSpoolmanSettings,
   });
   });
 
 
   // Fetch Spoolman status
   // Fetch Spoolman status
@@ -93,7 +67,7 @@ export function SpoolmanSettings() {
   // Save mutation
   // Save mutation
   const saveMutation = useMutation({
   const saveMutation = useMutation({
     mutationFn: () =>
     mutationFn: () =>
-      updateSpoolmanSettings({
+      api.updateSpoolmanSettings({
         spoolman_enabled: localEnabled ? 'true' : 'false',
         spoolman_enabled: localEnabled ? 'true' : 'false',
         spoolman_url: localUrl,
         spoolman_url: localUrl,
         spoolman_sync_mode: localSyncMode,
         spoolman_sync_mode: localSyncMode,

+ 11 - 14
frontend/src/pages/CameraPage.tsx

@@ -224,21 +224,18 @@ export function CameraPage() {
     // Start stall detection after stream has loaded
     // Start stall detection after stream has loaded
     stallCheckIntervalRef.current = setInterval(async () => {
     stallCheckIntervalRef.current = setInterval(async () => {
       try {
       try {
-        const response = await fetch(`/api/v1/printers/${id}/camera/status`);
-        if (response.ok) {
-          const status = await response.json();
-          // Trigger reconnect if:
-          // 1. Backend reports stall (no frames for 10+ seconds)
-          // 2. OR stream is not active anymore (process died)
-          if (status.stalled || (!status.active && !streamError)) {
-            console.log(`Stream issue detected: stalled=${status.stalled}, active=${status.active}, reconnecting...`);
-            if (stallCheckIntervalRef.current) {
-              clearInterval(stallCheckIntervalRef.current);
-              stallCheckIntervalRef.current = null;
-            }
-            setStreamLoading(false);
-            attemptReconnect();
+        const status = await api.getCameraStatus(id);
+        // Trigger reconnect if:
+        // 1. Backend reports stall (no frames for 10+ seconds)
+        // 2. OR stream is not active anymore (process died)
+        if (status.stalled || (!status.active && !streamError)) {
+          console.log(`Stream issue detected: stalled=${status.stalled}, active=${status.active}, reconnecting...`);
+          if (stallCheckIntervalRef.current) {
+            clearInterval(stallCheckIntervalRef.current);
+            stallCheckIntervalRef.current = null;
           }
           }
+          setStreamLoading(false);
+          attemptReconnect();
         }
         }
       } catch {
       } catch {
         // Ignore fetch errors - server might be temporarily unavailable
         // Ignore fetch errors - server might be temporarily unavailable

+ 2 - 10
frontend/src/pages/ProjectDetailPage.tsx

@@ -404,19 +404,11 @@ export function ProjectDetailPage() {
 
 
   const handleExportProject = async () => {
   const handleExportProject = async () => {
     try {
     try {
-      // Fetch ZIP file directly
-      const response = await fetch(`/api/v1/projects/${projectId}/export`);
-      if (!response.ok) {
-        throw new Error(t('projectDetail.toast.exportFailed'));
-      }
-      const blob = await response.blob();
+      const { blob, filename } = await api.exportProjectZip(Number(projectId));
       const url = URL.createObjectURL(blob);
       const url = URL.createObjectURL(blob);
       const a = document.createElement('a');
       const a = document.createElement('a');
       a.href = url;
       a.href = url;
-      // Get filename from Content-Disposition header or use default
-      const contentDisposition = response.headers.get('Content-Disposition');
-      const filenameMatch = contentDisposition?.match(/filename="(.+)"/);
-      a.download = filenameMatch?.[1] || `${project?.name || 'project'}_${new Date().toISOString().split('T')[0]}.zip`;
+      a.download = filename || `${project?.name || 'project'}_${new Date().toISOString().split('T')[0]}.zip`;
       a.click();
       a.click();
       URL.revokeObjectURL(url);
       URL.revokeObjectURL(url);
       showToast(t('projectDetail.toast.projectExported'), 'success');
       showToast(t('projectDetail.toast.projectExported'), 'success');

+ 1 - 13
frontend/src/pages/ProjectsPage.tsx

@@ -681,19 +681,7 @@ export function ProjectsPage() {
 
 
       if (filename.endsWith('.zip')) {
       if (filename.endsWith('.zip')) {
         // ZIP file: upload via file endpoint
         // ZIP file: upload via file endpoint
-        const formData = new FormData();
-        formData.append('file', file);
-
-        const response = await fetch('/api/v1/projects/import/file', {
-          method: 'POST',
-          body: formData,
-        });
-
-        if (!response.ok) {
-          const errorData = await response.json();
-          throw new Error(errorData.detail || 'Import failed');
-        }
-
+        await api.importProjectFile(file);
         queryClient.invalidateQueries({ queryKey: ['projects'] });
         queryClient.invalidateQueries({ queryKey: ['projects'] });
         showToast(t('projects.toast.imported'), 'success');
         showToast(t('projects.toast.imported'), 'success');
       } else {
       } else {

ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/index-C_9mu6sD.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/index-DYRi1tMz.js


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-DOAYfAw5.js"></script>
+    <script type="module" crossorigin src="/assets/index-C_9mu6sD.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-DME4t7XG.css">
     <link rel="stylesheet" crossorigin href="/assets/index-DME4t7XG.css">
   </head>
   </head>
   <body>
   <body>

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません