Browse Source

Post work PR #262

maziggy 3 months ago
parent
commit
6a35e945ce

+ 8 - 1
frontend/src/components/GcodeViewer.tsx

@@ -1,6 +1,7 @@
 import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
 import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
 import { WebGLPreview } from 'gcode-preview';
 import { WebGLPreview } from 'gcode-preview';
 import { Loader2, Layers, ChevronLeft, ChevronRight, FileWarning } from 'lucide-react';
 import { Loader2, Layers, ChevronLeft, ChevronRight, FileWarning } from 'lucide-react';
+import { getAuthToken } from '../api/client';
 
 
 interface GcodeViewerProps {
 interface GcodeViewerProps {
   gcodeUrl: string;
   gcodeUrl: string;
@@ -63,7 +64,13 @@ export function GcodeViewer({
     previewRef.current = preview;
     previewRef.current = preview;
 
 
     // Fetch and process gcode
     // Fetch and process gcode
-    fetch(gcodeUrl)
+    const headers: HeadersInit = {};
+    const token = getAuthToken();
+    if (token) {
+      headers['Authorization'] = `Bearer ${token}`;
+    }
+
+    fetch(gcodeUrl, { headers })
       .then(async response => {
       .then(async response => {
         if (!response.ok) {
         if (!response.ok) {
           if (response.status === 404) {
           if (response.status === 404) {

+ 17 - 7
frontend/src/components/ModelViewer.tsx

@@ -1,4 +1,5 @@
 import { useEffect, useRef, useState } from 'react';
 import { useEffect, useRef, useState } from 'react';
+import { useTranslation } from 'react-i18next';
 import * as THREE from 'three';
 import * as THREE from 'three';
 import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
 import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
 import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
 import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
@@ -6,6 +7,7 @@ import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
 import JSZip from 'jszip';
 import JSZip from 'jszip';
 import { Loader2, RotateCcw, ZoomIn, ZoomOut } from 'lucide-react';
 import { Loader2, RotateCcw, ZoomIn, ZoomOut } from 'lucide-react';
 import { Button } from './Button';
 import { Button } from './Button';
+import { getAuthToken } from '../api/client';
 
 
 interface BuildVolume {
 interface BuildVolume {
   x: number;
   x: number;
@@ -585,6 +587,7 @@ export function ModelViewer({
   selectedPlateId = null,
   selectedPlateId = null,
   className = '',
   className = '',
 }: ModelViewerProps) {
 }: ModelViewerProps) {
+  const { t } = useTranslation();
   const containerRef = useRef<HTMLDivElement>(null);
   const containerRef = useRef<HTMLDivElement>(null);
   const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
   const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
   const sceneRef = useRef<THREE.Scene | null>(null);
   const sceneRef = useRef<THREE.Scene | null>(null);
@@ -677,10 +680,17 @@ export function ModelViewer({
 
 
     const normalizedType = (fileType || url.split('?')[0].split('.').pop() || '').toLowerCase();
     const normalizedType = (fileType || url.split('?')[0].split('.').pop() || '').toLowerCase();
 
 
+    // Build auth headers for fetch
+    const headers: HeadersInit = {};
+    const token = getAuthToken();
+    if (token) {
+      headers['Authorization'] = `Bearer ${token}`;
+    }
+
     if (normalizedType === 'stl') {
     if (normalizedType === 'stl') {
-      fetch(url)
+      fetch(url, { headers })
         .then((res) => {
         .then((res) => {
-          if (!res.ok) throw new Error('Failed to load file');
+          if (!res.ok) throw new Error(t('modelViewer.errors.failedToLoad'));
           return res.arrayBuffer();
           return res.arrayBuffer();
         })
         })
         .then((buffer) => {
         .then((buffer) => {
@@ -695,15 +705,15 @@ export function ModelViewer({
           setLoading(false);
           setLoading(false);
         });
         });
     } else if (normalizedType === '3mf') {
     } else if (normalizedType === '3mf') {
-      fetch(url)
+      fetch(url, { headers })
         .then((res) => {
         .then((res) => {
-          if (!res.ok) throw new Error('Failed to load file');
+          if (!res.ok) throw new Error(t('modelViewer.errors.failedToLoad'));
           return res.arrayBuffer();
           return res.arrayBuffer();
         })
         })
         .then(parse3MF)
         .then(parse3MF)
         .then((parsed) => {
         .then((parsed) => {
           if (parsed.objects.size === 0) {
           if (parsed.objects.size === 0) {
-            throw new Error('No meshes found in 3MF file');
+            throw new Error(t('modelViewer.errors.noMeshes'));
           }
           }
           setParsedData(parsed);
           setParsedData(parsed);
         })
         })
@@ -712,7 +722,7 @@ export function ModelViewer({
           setLoading(false);
           setLoading(false);
         });
         });
     } else {
     } else {
-      setError('Unsupported file format');
+      setError(t('modelViewer.errors.unsupportedFormat'));
       setLoading(false);
       setLoading(false);
     }
     }
 
 
@@ -743,7 +753,7 @@ export function ModelViewer({
       plateRef.current = null;
       plateRef.current = null;
       gridRef.current = null;
       gridRef.current = null;
     };
     };
-  }, [url, buildVolume, fileType]);
+  }, [url, buildVolume, fileType, t]);
 
 
   useEffect(() => {
   useEffect(() => {
     if (!sceneRef.current || !cameraRef.current || !controlsRef.current) return;
     if (!sceneRef.current || !cameraRef.current || !controlsRef.current) return;

+ 21 - 22
frontend/src/components/ModelViewerModal.tsx

@@ -1,4 +1,5 @@
 import { useState, useEffect, useRef, useMemo } from 'react';
 import { useState, useEffect, useRef, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
 import { X, ExternalLink, Box, Code2, Loader2, Layers, Check, Maximize2, Minimize2 } from 'lucide-react';
 import { X, ExternalLink, Box, Code2, Loader2, Layers, Check, Maximize2, Minimize2 } from 'lucide-react';
 import { ModelViewer } from './ModelViewer';
 import { ModelViewer } from './ModelViewer';
 import { GcodeViewer } from './GcodeViewer';
 import { GcodeViewer } from './GcodeViewer';
@@ -26,6 +27,7 @@ interface Capabilities {
 }
 }
 
 
 export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, onClose }: ModelViewerModalProps) {
 export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, onClose }: ModelViewerModalProps) {
+  const { t } = useTranslation();
   const isLibrary = libraryFileId != null;
   const isLibrary = libraryFileId != null;
   const [activeTab, setActiveTab] = useState<ViewTab | null>(null);
   const [activeTab, setActiveTab] = useState<ViewTab | null>(null);
   const [capabilities, setCapabilities] = useState<Capabilities | null>(null);
   const [capabilities, setCapabilities] = useState<Capabilities | null>(null);
@@ -139,7 +141,7 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
   const getPlateObjectCount = (plate: PlateMetadata): number => plate.object_count ?? plate.objects?.length ?? 0;
   const getPlateObjectCount = (plate: PlateMetadata): number => plate.object_count ?? plate.objects?.length ?? 0;
   const totalObjectCount = plates.reduce((sum, plate) => sum + getPlateObjectCount(plate), 0);
   const totalObjectCount = plates.reduce((sum, plate) => sum + getPlateObjectCount(plate), 0);
   const selectedObjectCount = selectedPlate ? getPlateObjectCount(selectedPlate) : totalObjectCount;
   const selectedObjectCount = selectedPlate ? getPlateObjectCount(selectedPlate) : totalObjectCount;
-  const objectCountLabel = selectedPlate ? `Plate ${selectedPlate.index}` : 'All plates';
+  const objectCountLabel = selectedPlate ? t('modelViewer.plateNumber', { number: selectedPlate.index }) : t('modelViewer.allPlates');
   const hasObjectCount = plates.length > 0;
   const hasObjectCount = plates.length > 0;
   const platesGridRef = useRef<HTMLDivElement>(null);
   const platesGridRef = useRef<HTMLDivElement>(null);
   const platesViewportRef = useRef<HTMLDivElement>(null);
   const platesViewportRef = useRef<HTMLDivElement>(null);
@@ -288,14 +290,14 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
             <h2 className="text-lg font-semibold text-white truncate">{title}</h2>
             <h2 className="text-lg font-semibold text-white truncate">{title}</h2>
             {hasObjectCount && (
             {hasObjectCount && (
               <span className="text-xs text-bambu-gray bg-bambu-dark-tertiary/70 px-2 py-1 rounded whitespace-nowrap">
               <span className="text-xs text-bambu-gray bg-bambu-dark-tertiary/70 px-2 py-1 rounded whitespace-nowrap">
-                {objectCountLabel}: {selectedObjectCount} object{selectedObjectCount !== 1 ? 's' : ''}
+                {objectCountLabel}: {t('modelViewer.objectCount', { count: selectedObjectCount })}
               </span>
               </span>
             )}
             )}
           </div>
           </div>
           <div className="flex items-center gap-2">
           <div className="flex items-center gap-2">
             <Button variant="secondary" size="sm" onClick={handleOpenInSlicer} disabled={!canOpenInSlicer}>
             <Button variant="secondary" size="sm" onClick={handleOpenInSlicer} disabled={!canOpenInSlicer}>
               <ExternalLink className="w-4 h-4" />
               <ExternalLink className="w-4 h-4" />
-              Open in Slicer
+              {t('modelViewer.openInSlicer')}
             </Button>
             </Button>
             <Button
             <Button
               variant="secondary"
               variant="secondary"
@@ -326,8 +328,8 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
               }`}
               }`}
             >
             >
               <Box className="w-4 h-4" />
               <Box className="w-4 h-4" />
-              3D Model
-              {!capabilities.has_model && <span className="text-xs">(not available)</span>}
+              {t('modelViewer.tabs.model')}
+              {!capabilities.has_model && <span className="text-xs">({t('modelViewer.notAvailable')})</span>}
             </button>
             </button>
             <button
             <button
               onClick={() => capabilities.has_gcode && setActiveTab('gcode')}
               onClick={() => capabilities.has_gcode && setActiveTab('gcode')}
@@ -341,8 +343,8 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
               }`}
               }`}
             >
             >
               <Code2 className="w-4 h-4" />
               <Code2 className="w-4 h-4" />
-              G-code Preview
-              {!capabilities.has_gcode && <span className="text-xs">(not sliced)</span>}
+              {t('modelViewer.tabs.gcode')}
+              {!capabilities.has_gcode && <span className="text-xs">({t('modelViewer.notSliced')})</span>}
             </button>
             </button>
           </div>
           </div>
         )}
         )}
@@ -366,7 +368,7 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
                 >
                 >
                   <div className="flex items-center gap-2 text-sm text-bambu-gray mb-2">
                   <div className="flex items-center gap-2 text-sm text-bambu-gray mb-2">
                     <Layers className="w-4 h-4" />
                     <Layers className="w-4 h-4" />
-                    Plates
+                    {t('modelViewer.plates')}
                     {platesLoading && <Loader2 className="w-3 h-3 animate-spin" />}
                     {platesLoading && <Loader2 className="w-3 h-3 animate-spin" />}
                   </div>
                   </div>
                   <div className={splitFullscreen ? 'flex flex-col min-h-0 flex-1' : undefined}>
                   <div className={splitFullscreen ? 'flex flex-col min-h-0 flex-1' : undefined}>
@@ -396,9 +398,9 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
                             <Layers className={`${splitFullscreen ? 'w-4 h-4' : 'w-5 h-5'} text-bambu-gray`} />
                             <Layers className={`${splitFullscreen ? 'w-4 h-4' : 'w-5 h-5'} text-bambu-gray`} />
                           </div>
                           </div>
                           <div className="min-w-0 flex-1">
                           <div className="min-w-0 flex-1">
-                            <p className={`${splitFullscreen ? 'text-xs' : 'text-sm'} text-white font-medium truncate`}>All Plates</p>
+                            <p className={`${splitFullscreen ? 'text-xs' : 'text-sm'} text-white font-medium truncate`}>{t('modelViewer.allPlates')}</p>
                             <p className={`${splitFullscreen ? 'text-[10px]' : 'text-xs'} text-bambu-gray truncate`}>
                             <p className={`${splitFullscreen ? 'text-[10px]' : 'text-xs'} text-bambu-gray truncate`}>
-                              {plates.length} plate{plates.length !== 1 ? 's' : ''}
+                              {t('modelViewer.plateCount', { count: plates.length })}
                             </p>
                             </p>
                           </div>
                           </div>
                           {selectedPlateId == null && (
                           {selectedPlateId == null && (
@@ -433,13 +435,10 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
                             )}
                             )}
                             <div className="min-w-0 flex-1">
                             <div className="min-w-0 flex-1">
                               <p className={`${splitFullscreen ? 'text-xs' : 'text-sm'} text-white font-medium truncate`}>
                               <p className={`${splitFullscreen ? 'text-xs' : 'text-sm'} text-white font-medium truncate`}>
-                                {plate.name || `Plate ${plate.index}`}
+                                {plate.name || t('modelViewer.plateNumber', { number: plate.index })}
                               </p>
                               </p>
                               <p className={`${splitFullscreen ? 'text-[10px]' : 'text-xs'} text-bambu-gray truncate`}>
                               <p className={`${splitFullscreen ? 'text-[10px]' : 'text-xs'} text-bambu-gray truncate`}>
-                                {(() => {
-                                  const objectCount = plate.object_count ?? plate.objects?.length ?? 0;
-                                  return `${objectCount} object${objectCount !== 1 ? 's' : ''}`;
-                                })()}
+                                {t('modelViewer.objectCount', { count: plate.object_count ?? plate.objects?.length ?? 0 })}
                               </p>
                               </p>
                             </div>
                             </div>
                             {selectedPlateId === plate.index && (
                             {selectedPlateId === plate.index && (
@@ -453,21 +452,21 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
                       <div className="mt-auto pt-3 flex items-center gap-4 text-xs text-bambu-gray overflow-x-auto">
                       <div className="mt-auto pt-3 flex items-center gap-4 text-xs text-bambu-gray overflow-x-auto">
                         {selectedPlate && (
                         {selectedPlate && (
                           <div className="flex items-center gap-3 whitespace-nowrap">
                           <div className="flex items-center gap-3 whitespace-nowrap">
-                            <span>Plate {selectedPlate.index}</span>
+                            <span>{t('modelViewer.plateNumber', { number: selectedPlate.index })}</span>
                             {selectedPlate.print_time_seconds != null && (
                             {selectedPlate.print_time_seconds != null && (
-                              <span>ETA {Math.round(selectedPlate.print_time_seconds / 60)} min</span>
+                              <span>{t('modelViewer.eta', { minutes: Math.round(selectedPlate.print_time_seconds / 60) })}</span>
                             )}
                             )}
                             {selectedPlate.filament_used_grams != null && (
                             {selectedPlate.filament_used_grams != null && (
                               <span>{selectedPlate.filament_used_grams.toFixed(1)} g</span>
                               <span>{selectedPlate.filament_used_grams.toFixed(1)} g</span>
                             )}
                             )}
                             {selectedPlate.filaments.length > 0 && (
                             {selectedPlate.filaments.length > 0 && (
-                              <span>{selectedPlate.filaments.length} filament{selectedPlate.filaments.length !== 1 ? 's' : ''}</span>
+                              <span>{t('modelViewer.filamentCount', { count: selectedPlate.filaments.length })}</span>
                             )}
                             )}
                           </div>
                           </div>
                         )}
                         )}
                         {shouldPaginatePlates && (
                         {shouldPaginatePlates && (
                           <div className={`flex items-center gap-2 whitespace-nowrap ${selectedPlate ? 'ml-auto' : ''}`}>
                           <div className={`flex items-center gap-2 whitespace-nowrap ${selectedPlate ? 'ml-auto' : ''}`}>
-                            <span>Page {platePage + 1} of {totalPlatePages}</span>
+                            <span>{t('modelViewer.pagination.pageOf', { current: platePage + 1, total: totalPlatePages })}</span>
                             <div className="flex items-center gap-1">
                             <div className="flex items-center gap-1">
                               <button
                               <button
                                 type="button"
                                 type="button"
@@ -479,7 +478,7 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
                                     : 'border-bambu-dark-tertiary text-bambu-gray hover:text-white hover:border-bambu-gray'
                                     : 'border-bambu-dark-tertiary text-bambu-gray hover:text-white hover:border-bambu-gray'
                                 }`}
                                 }`}
                               >
                               >
-                                Prev
+                                {t('modelViewer.pagination.prev')}
                               </button>
                               </button>
                               {(() => {
                               {(() => {
                                 const maxVisible = 5;
                                 const maxVisible = 5;
@@ -547,7 +546,7 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
                                     : 'border-bambu-dark-tertiary text-bambu-gray hover:text-white hover:border-bambu-gray'
                                     : 'border-bambu-dark-tertiary text-bambu-gray hover:text-white hover:border-bambu-gray'
                                 }`}
                                 }`}
                               >
                               >
-                                Next
+                                {t('modelViewer.pagination.next')}
                               </button>
                               </button>
                             </div>
                             </div>
                           </div>
                           </div>
@@ -596,7 +595,7 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
             />
             />
           ) : (
           ) : (
             <div className="w-full h-full flex items-center justify-center text-bambu-gray">
             <div className="w-full h-full flex items-center justify-center text-bambu-gray">
-              No preview available for this file
+              {t('modelViewer.noPreview')}
             </div>
             </div>
           )}
           )}
         </div>
         </div>

+ 32 - 0
frontend/src/i18n/locales/de.ts

@@ -2543,6 +2543,38 @@ export default {
     },
     },
   },
   },
 
 
+  // Model Viewer
+  modelViewer: {
+    openInSlicer: 'Im Slicer öffnen',
+    tabs: {
+      model: '3D-Modell',
+      gcode: 'G-Code Vorschau',
+    },
+    notAvailable: 'nicht verfügbar',
+    notSliced: 'nicht geslicet',
+    plates: 'Platten',
+    allPlates: 'Alle Platten',
+    plateNumber: 'Platte {{number}}',
+    plateCount: '{{count}} Platte',
+    plateCount_other: '{{count}} Platten',
+    objectCount: '{{count}} Objekt',
+    objectCount_other: '{{count}} Objekte',
+    filamentCount: '{{count}} Filament',
+    filamentCount_other: '{{count}} Filamente',
+    eta: 'ETA {{minutes}} Min',
+    noPreview: 'Keine Vorschau für diese Datei verfügbar',
+    pagination: {
+      pageOf: 'Seite {{current}} von {{total}}',
+      prev: 'Zurück',
+      next: 'Weiter',
+    },
+    errors: {
+      failedToLoad: 'Datei konnte nicht geladen werden',
+      noMeshes: 'Keine Meshes in 3MF-Datei gefunden',
+      unsupportedFormat: 'Nicht unterstütztes Dateiformat',
+    },
+  },
+
   // Maintenance type descriptions (built-in)
   // Maintenance type descriptions (built-in)
   maintenanceDescriptions: {
   maintenanceDescriptions: {
     lubricateRails: 'Schmiermittel auf Linearschienen für sanfte Bewegung auftragen',
     lubricateRails: 'Schmiermittel auf Linearschienen für sanfte Bewegung auftragen',

+ 32 - 0
frontend/src/i18n/locales/en.ts

@@ -2543,6 +2543,38 @@ export default {
     },
     },
   },
   },
 
 
+  // Model Viewer
+  modelViewer: {
+    openInSlicer: 'Open in Slicer',
+    tabs: {
+      model: '3D Model',
+      gcode: 'G-code Preview',
+    },
+    notAvailable: 'not available',
+    notSliced: 'not sliced',
+    plates: 'Plates',
+    allPlates: 'All Plates',
+    plateNumber: 'Plate {{number}}',
+    plateCount: '{{count}} plate',
+    plateCount_other: '{{count}} plates',
+    objectCount: '{{count}} object',
+    objectCount_other: '{{count}} objects',
+    filamentCount: '{{count}} filament',
+    filamentCount_other: '{{count}} filaments',
+    eta: 'ETA {{minutes}} min',
+    noPreview: 'No preview available for this file',
+    pagination: {
+      pageOf: 'Page {{current}} of {{total}}',
+      prev: 'Prev',
+      next: 'Next',
+    },
+    errors: {
+      failedToLoad: 'Failed to load file',
+      noMeshes: 'No meshes found in 3MF file',
+      unsupportedFormat: 'Unsupported file format',
+    },
+  },
+
   // Maintenance type descriptions (built-in)
   // Maintenance type descriptions (built-in)
   maintenanceDescriptions: {
   maintenanceDescriptions: {
     lubricateRails: 'Apply lubricant to linear rails for smooth motion',
     lubricateRails: 'Apply lubricant to linear rails for smooth motion',

+ 32 - 0
frontend/src/i18n/locales/ja.ts

@@ -3074,4 +3074,36 @@ export default {
     failedToCreateBackup: 'バックアップの作成に失敗しました',
     failedToCreateBackup: 'バックアップの作成に失敗しました',
     backupRestored: 'バックアップの復元が完了しました',
     backupRestored: 'バックアップの復元が完了しました',
   },
   },
+
+  // Model Viewer
+  modelViewer: {
+    openInSlicer: 'スライサーで開く',
+    tabs: {
+      model: '3Dモデル',
+      gcode: 'G-codeプレビュー',
+    },
+    notAvailable: '利用不可',
+    notSliced: '未スライス',
+    plates: 'プレート',
+    allPlates: '全プレート',
+    plateNumber: 'プレート {{number}}',
+    plateCount: '{{count}} プレート',
+    plateCount_other: '{{count}} プレート',
+    objectCount: '{{count}} オブジェクト',
+    objectCount_other: '{{count}} オブジェクト',
+    filamentCount: '{{count}} フィラメント',
+    filamentCount_other: '{{count}} フィラメント',
+    eta: '予想時間 {{minutes}} 分',
+    noPreview: 'このファイルのプレビューは利用できません',
+    pagination: {
+      pageOf: 'ページ {{current}} / {{total}}',
+      prev: '前へ',
+      next: '次へ',
+    },
+    errors: {
+      failedToLoad: 'ファイルの読み込みに失敗しました',
+      noMeshes: '3MFファイルにメッシュが見つかりません',
+      unsupportedFormat: 'サポートされていないファイル形式です',
+    },
+  },
 };
 };

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-CosC5iN4.css


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-mqcja8gj.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-qXxId8sa.css


+ 2 - 2
static/index.html

@@ -23,8 +23,8 @@
 
 
     <!-- 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-zuNzNq45.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-qXxId8sa.css">
+    <script type="module" crossorigin src="/assets/index-mqcja8gj.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-CosC5iN4.css">
   </head>
   </head>
   <body>
   <body>
     <div id="root"></div>
     <div id="root"></div>

+ 1 - 0
test_frontend.sh

@@ -1,6 +1,7 @@
 #!/bin/sh
 #!/bin/sh
 
 
 cd frontend
 cd frontend
+npx tsc
 npm run lint
 npm run lint
 npm run test:run
 npm run test:run
 cd ..
 cd ..

Some files were not shown because too many files changed in this diff