diff --git a/.gitignore b/.gitignore index 2d5d515..5c125da 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ __pycache__ *:Zone.Identifier + # C extensions *.so diff --git a/Livrable/README.md b/Livrable/README.md new file mode 100644 index 0000000..2c1a36d --- /dev/null +++ b/Livrable/README.md @@ -0,0 +1,6 @@ +# Livrables + +Générer un pdf à partir de .md (Markdown): +sudo apt install texlive-latex-recommended texlive-fonts-recommended +sudo apt install pandoc +pandoc [nom du fichier markdown].md -o [nom du fichier pdf à générer].pdf --toc -V geometry:margin=1in \ No newline at end of file diff --git a/diagrams/auth-diagram.puml b/Livrable/diagrams/auth-diagram.puml similarity index 87% rename from diagrams/auth-diagram.puml rename to Livrable/diagrams/auth-diagram.puml index 90b7398..21efd56 100644 --- a/diagrams/auth-diagram.puml +++ b/Livrable/diagrams/auth-diagram.puml @@ -1,6 +1,10 @@ @startuml -title "Sequencial Diagram \n Authentification" - +title Sequencial Diagram\nAuthentification +scale 0.5 +skinparam monochrome true +skinparam shadowing false +skinparam defaultFontName Arial +skinparam linetype ortho skinparam style strictuml skinparam sequenceMessageAlign center diff --git a/Livrable/diagrams/auth-diagram.svg b/Livrable/diagrams/auth-diagram.svg new file mode 100644 index 0000000..2b10337 --- /dev/null +++ b/Livrable/diagrams/auth-diagram.svg @@ -0,0 +1,70 @@ +Sequencial DiagramAuthentificationUserAngular/Front-endDjango API (back.apps.api)PostgreSQLSign upFill the formPOST /api/v1/user/Check the mailalt[mail already been used]Conflict400 Bad RequestDisplay "This mail already been used"[new user]Save User (hash password)Success201 CreatedDisplay "Successful registration"Log in (with SimpleJWT)Enter identifiersPOST /api/v1/token/Verify Useralt[invalid identifiers]Not found / Wrong password401 UnauthorizedDisplay "Email or password wrong"[valid identifiers]200 OK {access, refresh}Tokens are stored in localStorage/Cookies/storeRedirect to main pageAPI call with expired access tokenGET /api/v1/user/me/ (expired access token)401 Unauthorizedalt[valid refresh token]POST /api/v1/token/refresh/ {refresh_token}200 OK {new_access_token}Retry the previous request[expired refresh token]Forced disconnection (redirect to login page) \ No newline at end of file diff --git a/Livrable/diagrams/bpmn-after-diagram.bpmn b/Livrable/diagrams/bpmn-after-diagram.bpmn new file mode 100644 index 0000000..7fcc17e --- /dev/null +++ b/Livrable/diagrams/bpmn-after-diagram.bpmn @@ -0,0 +1,646 @@ + + + + + + + + + + + + Activity_0dpaspu + Activity_0qeh398 + Activity_093xwnx + Activity_0m61fnb + Activity_1dcz8s2 + Activity_1fux38k + Activity_00eu04s + Activity_19f113s + Gateway_0khs5uj + Activity_165pyw1 + Activity_0e0zkxc + Event_18yyuts + Activity_18bfk0c + Gateway_1qe14yf + Activity_127okf6 + Activity_1dxkbqw + Gateway_12cbbt2 + Gateway_1czvu7n + Activity_0p0v089 + Activity_1xebvgp + Activity_03pdsld + + + Activity_0dpaspu + Activity_0qeh398 + Activity_0m61fnb + Activity_1fux38k + Activity_00eu04s + Activity_165pyw1 + Activity_0e0zkxc + Event_18yyuts + Gateway_12cbbt2 + Gateway_1czvu7n + Activity_0p0v089 + Activity_03pdsld + + + Activity_0dpaspu + Activity_0qeh398 + Activity_0m61fnb + Activity_1fux38k + Activity_00eu04s + Activity_165pyw1 + Activity_0e0zkxc + Event_18yyuts + Gateway_12cbbt2 + Gateway_1czvu7n + Activity_0p0v089 + Activity_03pdsld + + + + + Activity_093xwnx + Activity_1dcz8s2 + Activity_19f113s + Gateway_0khs5uj + Activity_18bfk0c + Gateway_1qe14yf + Activity_127okf6 + Activity_1dxkbqw + Activity_1xebvgp + + + Activity_093xwnx + Activity_1dcz8s2 + Activity_19f113s + Gateway_0khs5uj + Activity_18bfk0c + Gateway_1qe14yf + Activity_127okf6 + Activity_1dxkbqw + Activity_1xebvgp + + + + + + + Event_1h6yzyy + Gateway_0wp4z3s + Activity_1mywws0 + Activity_0idlddy + Event_088sduf + Activity_0b9ngql + Activity_1gmbuql + Event_1he5joh + + + Event_1h6yzyy + Gateway_0wp4z3s + Activity_1mywws0 + Activity_0idlddy + Event_088sduf + Activity_0b9ngql + Activity_1gmbuql + Event_1he5joh + + + Event_1h6yzyy + Gateway_0wp4z3s + Activity_1mywws0 + Activity_0idlddy + Event_088sduf + Activity_0b9ngql + Activity_1gmbuql + Event_1he5joh + + + + + + + + Flow_0tqrgl8 + Flow_0gvuic6 + Flow_0rlsafp + + + Flow_11qy4a3 + Flow_0gvuic6 + + + Flow_1thrgu2 + Flow_1fkkjlf + + + Flow_18srkl3 + Flow_1enud4a + + + Flow_0hk1oau + Flow_1a4wx38 + + + Flow_1a4wx38 + Flow_18srkl3 + + + Flow_1cfvcd4 + + + Flow_1veafjz + Flow_1s6mann + Flow_1fkkjlf + Flow_16jnbnx + Flow_0uxlsjr + + + Flow_0uxlsjr + Flow_1cfvcd4 + + + Flow_0i50jsu + Flow_1veafjz + + + Flow_15lw4za + Flow_1s6mann + + + Flow_0mq6v4v + Flow_0i50jsu + Flow_15lw4za + Flow_1thrgu2 + Flow_0v14q3e + + + Flow_0rlsafp + Flow_0gu13bk + + + Flow_0t5an5y + Flow_1vg43xc + + + Flow_1s0ets4 + Flow_1p606x5 + + + + Flow_0g2g0ln + Flow_11qy4a3 + + + Flow_1a9kmgh + Flow_0katfdb + Flow_0g2g0ln + + + Flow_0katfdb + Flow_0mq6v4v + + + Flow_1enud4a + Flow_1a9kmgh + + + Flow_1vg43xc + Flow_07fyk72 + + + Flow_08cpx6d + Flow_0j008cs + Flow_0tqrgl8 + + + Flow_0j008cs + Flow_07fyk72 + Flow_0hk1oau + Flow_0t5an5y + + + Flow_1p606x5 + Flow_108f4ql + + + Flow_108f4ql + Flow_08cpx6d + + + Flow_1f8vy1i + + + + + Flow_1f8vy1i + Flow_12txhya + + DataObjectReference_1j3rxed + + + + Flow_12txhya + Flow_1s0ets4 + + + Flow_0gu13bk + + + Flow_0v14q3e + Flow_16jnbnx + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Livrable/diagrams/bpmn-after-diagram.svg b/Livrable/diagrams/bpmn-after-diagram.svg new file mode 100644 index 0000000..8b4107b --- /dev/null +++ b/Livrable/diagrams/bpmn-after-diagram.svg @@ -0,0 +1,4 @@ + + + +Nouveau processus d'impression 3DClientClient ( Étudiant adhérent du campus )Association IntechOpérateurApplication Web PrIntechAnnule la requête[F04-4] Rembourse le compte de l'étudiant[F07-2] Note l'état de l'imprimante en libre[F07-2] Note l'état de l'imprimante en utilisée[F04-2] Sélectionne une imprimante libre pour faire l'impression[F04-4] Débite le solde du compteSuccèsRécupère son impression[F05-2] Notifie l'étudiant de la fin de l'impressionDécolle l'impression et la stocke dans le local[F05-2] Notifier l'étudiant de l'annulation de sa requête[F05-3] Redirection vers HelloAsso pour recréditer le compteCréer un délai de timeout pour éviter le flood[F04-3] Annule la requêteImpression réussie ?[F04-2] Valide la fin de l'impression[F03] Lance l'impression[F05-3] Paiement afin de recréditer lecompte[F03-6] Solde suffisant ?[F04-2] Sauvegarde la requête et l'affiche dans l'espace admin[F03-3] Renseigne le prix de la requêteBesoin d'une oude plusieurs pièces 3dFICHIER(S).stlCherche un/des bon(s) modèle(s) imprimable(s)[F03-1] Dépose son/ses fichierset crée une requêteÉchec[F07-2] Note l'état de l'imprimante en libreNONOUINONNONOUIOUIHelloAsso[F07-2] quantitésuffisante de bobine ? \ No newline at end of file diff --git a/Livrable/diagrams/bpmn-before-diagram.bpmn b/Livrable/diagrams/bpmn-before-diagram.bpmn new file mode 100644 index 0000000..b720d7d --- /dev/null +++ b/Livrable/diagrams/bpmn-before-diagram.bpmn @@ -0,0 +1,590 @@ + + + + + + + + + Stocke l'historique d'impression avec les fichiers stl + + + + + + + Event_1g7jk24 + Activity_02e9120 + Activity_1rl57cf + Activity_0sxor2z + Activity_1riyu65 + Gateway_1awjkog + Gateway_0ok16j5 + Activity_1xcpqxg + Gateway_1bgpa7l + Event_113t3c3 + Gateway_002zet6 + Activity_03123sj + + + Event_1g7jk24 + Activity_02e9120 + Activity_1rl57cf + Activity_0sxor2z + Activity_1riyu65 + Gateway_1awjkog + Gateway_0ok16j5 + Activity_1xcpqxg + Gateway_1bgpa7l + Event_113t3c3 + Gateway_002zet6 + Activity_03123sj + + + + + Gateway_0yd61w6 + Gateway_0ty28lf + Activity_1sbdp6r + Event_112zf02 + Event_0s7mfu5 + Activity_1sj5v2a + Activity_1rxh5wo + Gateway_0vcrxau + Event_16jwlwv + Event_02f503o + Gateway_19fnozy + Event_12h23ul + Activity_1jopk08 + Event_17j4tcb + Activity_1kuksoq + + + Gateway_0yd61w6 + Gateway_0ty28lf + Activity_1sbdp6r + Event_112zf02 + Event_0s7mfu5 + Activity_1sj5v2a + Activity_1rxh5wo + Gateway_0vcrxau + Event_16jwlwv + Event_02f503o + Gateway_19fnozy + Event_12h23ul + Activity_1jopk08 + Event_17j4tcb + Activity_1kuksoq + + + + + + Flow_1l54ezh + + + Flow_1l54ezh + Flow_0irrs2a + + DataObjectReference_0ht5mdb + + + + + + Flow_0irrs2a + Flow_0vojyl6 + + DataObjectReference_1mc76e4 + + + + + + Flow_0vojyl6 + Flow_1vs9oiw + + DataStoreReference_0o2nikv + + + + + Flow_1vs9oiw + Flow_12r2u93 + + + Flow_12r2u93 + Flow_05wg4rc + Flow_12m9k3r + + + Flow_05wg4rc + Flow_06ocrll + Flow_1d8ctse + + + Flow_0yh3euq + Flow_1d8ctse + Flow_0oqntxu + + + Flow_12m9k3r + Flow_0oqntxu + Flow_0ig3uqf + + + Flow_06ocrll + Flow_0labrkl + + + Flow_0labrkl + Flow_0yh3euq + + + Flow_0rwphor + + + + Flow_0rwphor + Flow_0wdd5jh + + + Flow_0ig3uqf + Flow_0ynfn1g + + + Flow_0wdd5jh + Flow_09xxsc9 + Flow_0xbjkzd + + + Flow_0xbjkzd + Flow_138zm0x + Flow_0j27l3a + + + Flow_0ynfn1g + Flow_1jhjqhw + Flow_1etiiob + + + Flow_1etiiob + Flow_1pkvgbt + + + + Flow_1pkvgbt + Flow_1vhoqbl + Flow_1jhjqhw + + + Flow_1vhoqbl + + + Flow_0j27l3a + Flow_01icipk + + + + Flow_138zm0x + Flow_0mlcyqz + + + + Flow_01icipk + Flow_09xxsc9 + Flow_0hawi0o + + + Flow_03m8ui3 + + + Flow_0mlcyqz + Flow_1azu3lz + + + Flow_1azu3lz + + + Flow_0hawi0o + Flow_03m8ui3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Livrable/diagrams/bpmn-before-diagram.svg b/Livrable/diagrams/bpmn-before-diagram.svg new file mode 100644 index 0000000..c875a52 --- /dev/null +++ b/Livrable/diagrams/bpmn-before-diagram.svg @@ -0,0 +1,4 @@ + + + +Ancien processus d'impression 3DOpérateur ( Association Intech )Client ( Étudiant adhérent du campus )Besoin d'unepièce 3dCherche un bonmodèle imprimableFICHIER.stlNommer le fichier d'impression avec son vrai nomNOM-PRENOM.stlRemplir le Google Form fournit par InTechFichier Google SheetsLance l'impressionUtilisateur présentquand l'impression est finie ?Machine requisepour une autre impression ?Décolle la pièceet la stocke dans le localAttendre le retourde l'étudiantFin du moisÉpluche le Sheets et calcule le total dû par étudiantRécupère son impressionEnvoie une demande de paiement et ajoute 1 au compteur de relancesReçoit la demande de remboursementPaye-t-il ?Paye7j s'écoulentl'étudiant règle sa dette avant 7jNombre de relance maximal atteint ?ÉchecValide le paiement et l'ajoute à sa comptabilitéSuccèsMise sur liste noire de l'étudiantNONOUIOUINONNONNONOUIOUIHelloAssoStocke l'historique d'impression avec les fichiers stlConfigure le HelloAsso pour chaque paiementpaiement via le lien donné par letrésorier de l'association \ No newline at end of file diff --git a/Livrable/diagrams/db-diagram.png b/Livrable/diagrams/db-diagram.png new file mode 100644 index 0000000..3a553f2 Binary files /dev/null and b/Livrable/diagrams/db-diagram.png differ diff --git a/Livrable/diagrams/db-diagram.puml b/Livrable/diagrams/db-diagram.puml new file mode 100644 index 0000000..c851d82 --- /dev/null +++ b/Livrable/diagrams/db-diagram.puml @@ -0,0 +1,140 @@ +@startuml +title UML Diagram\nDatabase Printech +scale 1.4 +hide circle +skinparam linetype ortho +skinparam shadowing false +skinparam roundcorner 10 +skinparam nodesep 80 +skinparam ranksep 80 +skinparam monochrome true +skinparam backgroundColor white +skinparam defaultFontName Segoe UI, Arial, sans-serif +skinparam style strictuml + +package "User Environment" #DDDDDD { + + enum UserRole <> { + ADHERENT + ROBOTECH + AUTOTECH + DRONE + BUREAU + } + + enum RequestStatus <> { + SUBMITTED + AWAITING_PAYMENT + PENDING + PRINTING + AWAITING_PICKUP + PICKED_UP + FAILED + CANCELED + } + + enum OperationType <> { + CASH + CARD + PAYMENT + REFUND + } + + entity "User" as user { + *id : UUID <> + -- + *username : varchar(150) <> + *email : varchar(254) <> + credit : integer = 0 + role : UserRole = ADHERENT + profile_picture : varchar(100) + } + + entity "File" as file { + *id : UUID <> + -- + filament_id : Bigint <> + *path : varchar(100) + number_of_printing : integer = 1 {>= 0} + para_slicer : jsonb + } + + entity "Request" as request { + *id : UUID <> + -- + *user_id : UUID <> + file_id : UUID <> + printer_id : varchar(25) <> + price : integer = 0 {>= 0} + comment : text + *created_at : DateTime + status : RequestStatus = SUBMITTED + } + + entity "Operation" as operation { + *id : UUID <> + -- + *beneficiary_id : UUID <> + *agent_id : UUID <> + request_id : UUID <> + *operation_type : OperationType + *amount : integer = 0 + comment : text + *created_at : DateTime + } +} + +package "Material Assets" #DDDDDD { + entity "Printer" as printer { + *name : varchar(25) <> + -- + status : PrinterStatus = DOWN + } + + entity "Filament" as filament { + *id : Bigint <> + -- + color : varchar(7) = '#ffffff' + *color_name : text + *type : FilamentType + quantity : integer = 0 {>= 0} + price : integer = 0 {>= 0} + } + + enum FilamentType <> { + PLA + PETG + } + + enum PrinterName <> { + CREALITY_K1C + SNAPMAKER_U1 + PRUSA_MK3 + } + + enum PrinterStatus <> { + UP + DOWN + USED + } +} + +' Relationships +user ||--o{ request : "places" + +user ||--o{ operation : "agent" + +file ||--o{ request : "is printed in" +filament ||-r-o{ file : "used by" +printer ||-r-o{ request : "processes" +request ||--o{ operation : "paid/refunded by" + +' Enum dependencies +user ..> UserRole : uses +request ..> RequestStatus : uses +operation ..> OperationType : uses +printer .u.> PrinterStatus : uses +printer .l.> PrinterName : uses +filament .l.> FilamentType : uses + +@enduml \ No newline at end of file diff --git a/Livrable/diagrams/db-diagram.svg b/Livrable/diagrams/db-diagram.svg new file mode 100644 index 0000000..6548c25 --- /dev/null +++ b/Livrable/diagrams/db-diagram.svg @@ -0,0 +1,3 @@ + +UML Diagram +Database PrintechUML DiagramDatabase PrintechUser EnvironmentMaterial Assets«enumeration»UserRoleADHERENTROBOTECHAUTOTECHDRONEBUREAU«enumeration»RequestStatusSUBMITTEDAWAITING_PAYMENTPENDINGPRINTINGAWAITING_PICKUPPICKED_UPFAILEDCANCELED«enumeration»OperationTypeCASHCARDPAYMENTREFUNDUserid : UUID «PK»username : varchar(150) «UNIQUE»email : varchar(254) «UNIQUE»credit : integer = 0role : UserRole = ADHERENTprofile_picture : varchar(100)Fileid : UUID «PK»filament_id : Bigint «FK»path : varchar(100)number_of_printing : integer = 1 {>= 0}para_slicer : jsonbRequestid : UUID «PK»user_id : UUID «FK»file_id : UUID «FK»printer_id : varchar(25) «FK»price : integer = 0 {>= 0}comment : textcreated_at : DateTimestatus : RequestStatus = SUBMITTEDOperationid : UUID «PK»beneficiary_id : UUID «FK»agent_id : UUID «FK»request_id : UUID «FK»operation_type : OperationTypeamount : integer = 0comment : textcreated_at : DateTimePrintername : varchar(25) «PK»status : PrinterStatus = DOWNFilamentid : Bigint «PK»color : varchar(7) = '#ffffff'color_name : texttype : FilamentTypequantity : integer = 0 {>= 0}price : integer = 0 {>= 0}«enumeration»FilamentTypePLAPETG«enumeration»PrinterNameCREALITY_K1CSNAPMAKER_U1PRUSA_MK3«enumeration»PrinterStatusUPDOWNUSEDplacesagentis printed inused byprocessespaid/refunded byusesusesusesusesusesuses \ No newline at end of file diff --git a/Livrable/output.pdf b/Livrable/output.pdf new file mode 100644 index 0000000..31506d1 Binary files /dev/null and b/Livrable/output.pdf differ diff --git a/Livrable/output.tex b/Livrable/output.tex new file mode 100644 index 0000000..c821340 --- /dev/null +++ b/Livrable/output.tex @@ -0,0 +1,964 @@ +\newpage + +\hypertarget{introduction}{% +\section{1. Introduction}\label{introduction}} + +Depuis cette année, l'association INTech du campus rencontrait des +difficultés de gestion des impressions 3D faites par les membres. En +effet, depuis cette année, INTech offre aux membres du campus la +possibilité d'imprimer leur modèle 3D en devenant adhérents suivant le +processus décrit dans la figure 1. + +Le processus commence lorsqu'un étudiant trouve un modèle 3D, le renomme +à son nom (au format \texttt{NOM-PRENOM.stl}) et remplit un Google Form +pour s'enregistrer dans un tableur, avant de lancer l'impression. Si +l'étudiant n'est pas présent à la fin du travail et que l'imprimante est +requise pour une autre tâche, un opérateur de l'association retire la +pièce et la stocke au local jusqu'au retour de l'étudiant qui vient +ensuite la récupérer. À la fin du mois, la phase de facturation +s'enclenche : l'opérateur consulte le tableur Google Sheets pour +calculer le montant total dû, crée un lien de paiement sur +\textbf{HelloAsso} et envoie la facture à l'étudiant. Si ce dernier paie +dans les 7 jours, l'opérateur valide la transaction dans sa +comptabilité, ce qui valide le succès du processus ; en revanche, s'il +ne règle pas sa dette après plusieurs relances de 7 jours et que le +plafond maximal d'avertissements est atteint, l'étudiant est inscrit sur +liste noire, ce qui entraîne l'échec de la procédure. + +Actuellement, le système de gestion possède des problèmes. Les adhérents +doivent télécharger et paramétrer un logiciel appelé slicer qui permet +de transformer un modèle 3D en série d'instructions pour les imprimantes +3D, appelé G-code. On demande ensuite aux adhérents de charger le G-code +sur les imprimantes, de lancer les impressions et ensuite de remplir un +Google Form avec le nom de leur fichier, le poids de l'impression et +d'autres paramètres. + +Ce système implique beaucoup d'étapes manuelles qui peuvent mener à des +erreurs et rendre la fraude facile. Il faut aussi former tout le monde +qui devient adhérent à l'utilisation des imprimantes. + +\begin{figure} +\centering +\includesvg{diagrams/bpmn-before-diagram.svg} +\caption{Diagramme BPMN du fonctionnement pré-PrIntech} +\end{figure} + +Intervient alors le projet PrINTech consistant au développement d'une +application web dynamique qui permette une gestion des impressions 3D de +manière automatique. + +Contrairement à la solution existante, ce projet a une vision plus long +terme et facilite l'expansion à plus d'imprimantes ou de personnes et +permet de réduire la charge de travail du bureau. + +\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center} + +\newpage + +\hypertarget{terminologie}{% +\subsection{1.1. Terminologie}\label{terminologie}} + +Pour faciliter la lecture de ce rapport, cette section regroupe et +définit les termes techniques, les acronymes et les notions spécifiques +au domaine de l'impression 3D et du développement web abordés dans ce +document. + +\hypertarget{impression-3d-et-matuxe9riaux}{% +\subsubsection{Impression 3D et +Matériaux}\label{impression-3d-et-matuxe9riaux}} + +\begin{itemize} +\tightlist +\item + \textbf{Fichier STL (\texttt{.stl})} : Format de fichier standard en + impression 3D (Stéréolithographie). Il représente la géométrie de la + surface d'un objet 3D sous forme de triangles. +\item + \textbf{Slicer (Trancheur)} : Logiciel qui découpe un modèle 3D + virtuel en fines couches horizontales et génère les instructions + nécessaires à l'imprimante. +\item + \textbf{G-code} : Langage de programmation numérique universel utilisé + pour piloter les machines-outils et les imprimantes 3D. +\item + \textbf{Filament} : Le fil de plastique en bobine qui sert de + ``matière première'' à l'imprimante 3D pour fabriquer les objets. +\item + \textbf{PETG} : Un type de plastique très résistant (le même que celui + des bouteilles d'eau, mais modifié), souvent utilisé pour des pièces + qui doivent supporter des chocs ou rester en extérieur. +\item + \textbf{PLA (Acide Polylactique)} : Plastique d'origine végétale + couramment utilisé en impression 3D. +\item + \textbf{Supports} : Structures temporaires imprimées en même temps que + le modèle. +\end{itemize} + +\hypertarget{technologies-web-architecture}{% +\subsubsection{Technologies Web \& +Architecture}\label{technologies-web-architecture}} + +\begin{itemize} +\tightlist +\item + \textbf{Angular} : Framework côté client (Frontend) utilisé pour + concevoir une interface utilisateur dynamique. +\item + \textbf{Django} : Framework de haut niveau basé sur Python, utilisé + pour le développement de la logique métier (Backend). +\item + \textbf{API REST} : Style d'architecture logicielle permettant au + Frontend et au Backend de communiquer via le protocole HTTP. +\item + \textbf{PostgreSQL} : Système de gestion de base de données + relationnelle open-source. +\item + \textbf{Docker \& Conteneurisation} : Technologie permettant d'isoler + une application et ses dépendances dans un environnement virtuel + appelé ``conteneur''. +\item + \textbf{Nginx \& Gunicorn} : Duo de production où Nginx sert de + serveur proxy inverse et Gunicorn de serveur d'application WSGI pour + exécuter le code Python. +\end{itemize} + +\hypertarget{outils-de-duxe9veloppement-tests}{% +\subsubsection{Outils de Développement \& +Tests}\label{outils-de-duxe9veloppement-tests}} + +\begin{itemize} +\tightlist +\item + \textbf{Git / Git Flow} : Git est le système de contrôle de version ; + Git Flow est la méthodologie qui structure la collaboration (branches + \texttt{main}, \texttt{develop}, \texttt{features/}). +\item + \textbf{PR (Pull Request)} : Demande formelle de fusion de code + soumise par un développeur pour relecture et validation. +\item + \textbf{uv} : Gestionnaire de paquets et d'environnements Python + extrêmement rapide écrit en Rust. +\item + \textbf{Playwright} : Framework de test de bout en bout (E2E) + permettant de simuler le comportement d'un utilisateur réel. +\end{itemize} + +\newpage + +\hypertarget{cahier-des-charges}{% +\section{2. Cahier des charges}\label{cahier-des-charges}} + +À la suite des rapports et des échanges menés avec le bureau et les +membres actifs de l'association INTech, une analyse approfondie des +besoins opérationnels a été réalisée. Le constat est simple : le volume +croissant de demandes d'impression 3D sur le campus sature le modèle de +gestion actuel, basé sur des processus manuels et des outils tiers +(Google Forms, tableurs Sheets). + +\hypertarget{description-du-sujet}{% +\subsection{2.1. Description du sujet}\label{description-du-sujet}} + +Notre projet consiste en la réalisation d'une Application Web dynamique. +C'est-à-dire que le site doit être capable d'intéragir avec une base de +donnée qu'on doit aussi implémenter. Ce site web permettra alors +l'identification sécurisé des étudiants voulant imprimer des modèles 3D +avec un système de gestion d'utilisateurs, de requêtes, et de gestion de +queues. Cette plateforme aura pour objectifs principaux : + +\begin{itemize} +\tightlist +\item + \textbf{L'authentification sécurisée} des étudiants du campus + souhaitant utiliser les services d'impression 3D. +\item + \textbf{La gestion des utilisateurs} en distinguant les rôles + (adhérents, bureau, membre d'un projet) et leurs droits associés. +\item + \textbf{Le traitement des requêtes} afin de centraliser, suivre et + historiser toutes les demandes de fabrication soumises par les élèves. +\item + \textbf{La gestion des crédits et de la tarification} afin de + permettre de payer chaque impression +\item + \textbf{Espace administrateur} Interface d'administration pour une + gestion complète +\end{itemize} + +\newpage + +\hypertarget{liste-des-fonctionnalituxe9s-de-lapplication}{% +\subsection{2.2. Liste des fonctionnalités de +l'application}\label{liste-des-fonctionnalituxe9s-de-lapplication}} + +Afin de répondre au sujet, l'application doit intégrer les +fonctionnalités suivantes, classées par priorité de développement : + +\begin{longtable}[]{@{} + >{\raggedright\arraybackslash}p{(\columnwidth - 6\tabcolsep) * \real{0.0870}} + >{\raggedright\arraybackslash}p{(\columnwidth - 6\tabcolsep) * \real{0.3261}} + >{\raggedright\arraybackslash}p{(\columnwidth - 6\tabcolsep) * \real{0.4783}} + >{\centering\arraybackslash}p{(\columnwidth - 6\tabcolsep) * \real{0.1087}}@{}} +\toprule\noalign{} +\begin{minipage}[b]{\linewidth}\raggedright +ID +\end{minipage} & \begin{minipage}[b]{\linewidth}\raggedright +Fonctionnalité de l'application +\end{minipage} & \begin{minipage}[b]{\linewidth}\raggedright +Description technique +\end{minipage} & \begin{minipage}[b]{\linewidth}\centering +Priorité +\end{minipage} \\ +\midrule\noalign{} +\endhead +\bottomrule\noalign{} +\endlastfoot +\textbf{F01} & \textbf{Authentification} & Connexion et déconnexion. & +Haute \\ +\textbf{F02} & \textbf{Gestion des profils} & Affichage et modification +des informations utilisateurs. & Basse \\ +\textbf{F03} & \textbf{Gestion des impressions} & Flux complet +d'impression 3D, du dépôt STL à la mise en file. & Haute \\ +\textbf{F03-1} & \textbf{Déposer un STL} & Upload d'un fichier STL. & +Haute \\ +\textbf{F03-2} & \textbf{Slicer automatique} & Convertir le modèle en +G-code via un slicer. & Moyenne \\ +\textbf{F03-3} & \textbf{Estimation du coût} & Calculer le prix +d'impression à partir des paramètres. & Basse \\ +\textbf{F03-4} & \textbf{Position dans la file} & Afficher la place du +travail dans la file. & Basse \\ +\textbf{F03-5} & \textbf{Priorité projets INTech} & Chaque utilisateur à +un rôle (adhérents, bureau, membre d'un projet) pour priiser les +impressions. & Basse \\ +\textbf{F03-6} & \textbf{Crédit restant} & Afficher le crédit restant +d'un utilisateur. & Basse \\ +\textbf{F03-7} & \textbf{Quantité d'impressions} & Indiquer le nombre +d'exemplaires à imprimer. & Basse \\ +\textbf{F04} & \textbf{Espace administrateur} & Interface +d'administration pour une gestion complète. & Moyenne \\ +\textbf{F04-1} & \textbf{Gestion des utilisateurs} & Créer, modifier et +supprimer les utilisateurs. & Haute \\ +\textbf{F04-2} & \textbf{Gestion des travaux} & Créer, modifier et +supprimer la file de travaux. & Moyenne \\ +\textbf{F04-3} & \textbf{Reprise des erreurs} & Déclarer un travail en +erreur et le remettre en file. & Moyenne \\ +\textbf{F04-4} & \textbf{Gestion crédit} & Gestion des crédits des +utilisateurs dont la recharge de crédits. & Haute \\ +\textbf{F05} & \textbf{Intégrations} & SSO et notifications externes. & +Pour aller plus loin \\ +\textbf{F05-1} & \textbf{Intégration CAS} & Inscription et connexion via +le CAS de l'école. & Pour aller plus loin \\ +\textbf{F05-2} & \textbf{Intégration Discord} & Utiliser des webhooks +pour les notifications. & Pour aller plus loin \\ +\textbf{F05-3} & \textbf{Intégration HelloAsso} & Créditer les comptes +via HelloAsso. & Pour aller plus loin \\ +\textbf{F06} & \textbf{Impression technique} & Options avancées +d'impression. & Pour aller plus loin \\ +\textbf{F06-1} & \textbf{Utilisation de supports} & Détecter quand +l'utilisation de supports est nécessaire. & Pour aller plus loin \\ +\textbf{F06-2} & \textbf{Paramétrage du slicer} & Permettre l'upload +d'un JSON de paramètres. & Pour aller plus loin \\ +\textbf{F06-3} & \textbf{Choix des matériaux} & Choisir un plastique +autre que le PLA. & Pour aller plus loin \\ +\textbf{F07} & \textbf{Gestion des filaments} & Administration des +filaments et du stock. & Pour aller plus loin \\ +\textbf{F07-1} & \textbf{Activation des filaments} & Activer et +désactiver des filaments. & Pour aller plus loin \\ +\textbf{F07-2} & \textbf{Gestion des filament} & Gérer la quantité de +filament restant. & Pour aller plus loin \\ +\textbf{F07-3} & \textbf{Gestion de consommation} & Rapport sur +quantités, crédits et montant imprimé. & Pour aller plus loin \\ +\end{longtable} + +\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center} + +\newpage + +\hypertarget{conception}{% +\section{3. Conception}\label{conception}} + +\hypertarget{base-de-donnuxe9es}{% +\subsection{3.1. Base de données}\label{base-de-donnuxe9es}} + +Comme l'illustre ce schema, lorsqu'un User (caractérisé par un rôle +applicatif et un solde de crédits) souhaite fabriquer un objet, il émet +une demande d'impression (Request). Celle-ci encapsule un fichier +numérique (File) contenant le modèle 3D et ses paramètres de découpage +en format JSON (para\_slicer), lui-même associé à un consommable précis +(Filament) défini par son type (PLA ou PETG) et sa couleur. La demande +est ensuite affectée à une machine (Printer) identifiée par son modèle +et son état de disponibilité. Enfin, pour éradiquer les risques de +fraude et automatiser la comptabilité, chaque transaction (recharge de +compte par carte/espèces ou débit/remboursement lié à une impression) +est tracée par l'entité Operation. Cette table lie un agent du bureau à +un utilisateur bénéficiaire tout en restant historiquement connectée à +la Request d'origine. + +\begin{figure} +\centering +\includesvg{diagrams/db-diagram.svg} +\caption{Schema de base de données} +\end{figure} + +\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center} + +\newpage + +\hypertarget{suxe9quence-de-lauthentification}{% +\subsection{3.2. Séquence de +l'Authentification}\label{suxe9quence-de-lauthentification}} + +Pour sécuriser l'accès aux ressources de la plateforme et garantir que +chaque étudiant interagit uniquement avec ses propres données, le projet +implémente un flux d'authentification basé sur des jetons d'accès (JWT) +via la librairie Django REST Framework SimpleJWT. Ce mécanisme élimine +les failles de sécurité de l'ancien système et permet une communication +fluide entre le Frontend Angular et l'API Backend. + +Le diagramme de séquence suivant détaille les phases d'inscription, de +connexion et de renouvellement des jetons : + +\begin{figure} +\centering +\includesvg{diagrams/auth-diagram.svg} +\caption{Schema de l'authentification} +\end{figure} + +\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center} + +\newpage + +\hypertarget{processus-dimpression-3d-avec-printech}{% +\subsection{3.3. Processus d'impression 3D avec +PrIntech}\label{processus-dimpression-3d-avec-printech}} + +\hypertarget{description-du-processus-dimpression-3d}{% +\subsubsection{Description du processus d'impression +3D}\label{description-du-processus-dimpression-3d}} + +Le processus débute lorsqu'un étudiant dépose un modèle 3D sur la +plateforme (\textbf{F03-1 : Déposer un STL}). Une fois la requête +enregistrée dans l'interface d'administration (\textbf{F04-2 : Gestion +des travaux}), le système évalue automatiquement les ressources +nécessaires. Il vérifie d'une part la quantité de matière disponible +(\textbf{F07-2 : Gestion des filaments}) et d'autre part le montant +estimé de l'impression (\textbf{F03-3 : Estimation du coût}) par rapport +aux ressources financières de l'utilisateur (\textbf{F03-6 : Crédit +restant}). Si le plastique est manquant ou indisponible, la demande est +arrêtée. Si l'utilisateur ne dispose pas de fonds suffisants, +l'application le redirige vers le module externe afin de renflouer son +compte (\textbf{F05-3 : Intégration HelloAsso}). + +Dès que ces vérifications sont validées, le travail rejoint la file +d'attente informatique et l'opérateur lui attribue une machine +disponible (\textbf{F04-2 : Gestion des travaux}), tout en prenant en +compte le rôle de l'utilisateur (\textbf{F03-5 : Priorité projets +INTech}). L'application procède alors au prélèvement des fonds requis +sur le compte de l'étudiant (\textbf{F04-4 : Gestion crédit}), met à +jour le statut informatique de l'imprimante, et l'opérateur lance +physiquement la fabrication de la pièce (\textbf{F03 : Gestion des +impressions}). À la fin du cycle, deux situations peuvent se présenter : +* \textbf{En cas de succès :} l'opérateur valide la fin de la tâche +(\textbf{F04-2 : Gestion des travaux}), l'imprimante repasse +informatiquement à l'état disponible, le stock de plastique restant est +ajusté (\textbf{F07-2 : Gestion des filaments}) et une alerte +automatisée est envoyée à l'étudiant (\textbf{F05-2 : Intégration +Discord}) pour qu'il vienne récupérer sa pièce. * \textbf{En cas d'échec +:} l'opérateur déclare l'avarie sur l'interface (\textbf{F04-3 : Reprise +des erreurs}), libérant immédiatement la machine. Le système réajuste +alors automatiquement le solde de l'étudiant (\textbf{F04-4 : Gestion +crédit}) tout en envoyant une notification pour l'avertir du problème +(\textbf{F05-2 : Intégration Discord}). + +\begin{figure} +\centering +\includesvg{diagrams/bpmn-after-diagram.svg} +\caption{Diagramme BPMN du fonctionnement post-PrIntech} +\end{figure} + +\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center} + +\newpage + +\hypertarget{ruxe9alisation-et-choix-techniques}{% +\section{4. Réalisation et choix +techniques}\label{ruxe9alisation-et-choix-techniques}} + +\hypertarget{choix-des-technologies}{% +\subsection{4.1. Choix des technologies}\label{choix-des-technologies}} + +Les technologies pour développer une Application Web sont nombreuses. +Pour faire la sélection des technologies, nous avons d'abord examiné +lesquelles étaient capables de répondre à notre cahier des charges, puis +ensuite sur les technologies auxquelles les membres de notre équipe +avaient déjà de l'expérience. Sur la base de ces critères, nous avons +sélectionné le stack suivant. D'abord pour le frontend : + +\begin{itemize} +\tightlist +\item + \textbf{Angular} : Pour la réalisation de l'interface utilisateur. + Angular s'appuie sur l'architecture de composants et sur le langage + \textbf{TypeScript}. Ce choix apporte un typage fort, une structure + rigoureuse. +\end{itemize} + +Ensuite pour le backend : + +\begin{itemize} +\item + \textbf{Django (Python)} : Utilisé pour développer le cœur logique du + backend et l'API REST (via \emph{Django REST Framework}). +\item + \textbf{uv} : Afin de gérer nos librairies et versions de python, on a + utilisé uv. C'est un équivalent de poetry mais en plus optimisé car + implémenté en Rust. On doit alors préciser avant chaque commande ``uv + run'' afin que le processus passe bien par uv. +\item + \textbf{Swagger (OpenAPI)} : Intégré à notre backend pour la + documentation et le test de l'API REST. Swagger génère automatiquement + une interface web interactive à partir des routes de notre application + Django. Cela permet aux développeurs frontend de comprendre + instantanément les points de terminaison (\emph{endpoints}), les + paramètres attendus et les réponses de l'API, facilitant ainsi + grandement la communication et l'intégration entre le frontend et le + backend. +\item + \textbf{PostgreSQL} : Ce système de gestion de base de données + relationnelle (SGBDR) open-source a été retenu pour sa robustesse, sa + conformité ACID et sa gestion fine des transactions financières + (crédits, remboursements, opérations). Sa capacité à indexer + efficacement les données structurées garantit des performances + optimales lors de la montée en charge du système et du suivi des files + d'attente de travaux. +\item + \textbf{Klipper} : comme firmware des imprimantes. Ce firmware + communautaire vient remplacer celui du constructeur +\item + \textbf{Moonraker} : expose les API JSON-RPC de Klipper avec une API + REST +\item + \textbf{FDM Monster} Cet orchestrateur open-source centralise la + gestion d'une flotte d'imprimantes 3D. Connecté aux différentes + instances Moonraker, il permet de piloter le parc, de centraliser le + contrôle des machines et de gérer globalement les files d'attente + d'impression. +\end{itemize} + +\hypertarget{tests-validation-et-qualituxe9-logicielle}{% +\subsubsection{Tests, Validation et Qualité +Logicielle}\label{tests-validation-et-qualituxe9-logicielle}} + +\begin{itemize} +\tightlist +\item + \textbf{Tests natifs Django (TestCase)} : Intégrés directement au + backend, le framework de test unitaire et d'intégration natif de + Django permet de sécuriser l'intégrité de la base de données lors des + transactions critiques. Ils valident de manière isolée les calculs de + prix, l'exactitude des débits et remboursements de crédits, le respect + des rôles utilisateurs pour la gestion des priorités, ainsi que les + transitions de statut des requêtes d'impression (du dépôt initial + jusqu'à la mise en file). +\item + \textbf{Playwright} : Framework moderne retenu pour l'automatisation + des tests de bout en bout (\emph{End-to-End}). Complémentaire aux + tests Django, Playwright nous permet de simuler le parcours complet + d'un utilisateur sur un navigateur réel de manière isolée. +\end{itemize} + +\hypertarget{conteneurisation-et-infrastructure}{% +\subsubsection{Conteneurisation et +Infrastructure}\label{conteneurisation-et-infrastructure}} + +\begin{itemize} +\tightlist +\item + \textbf{Docker \& Docker Compose} : Afin de partitionner logiquement + les différentes couches applicatives (Frontend, Backend, Base de + données), nous avons conteneurisé chaque service. Technologie + incontournable dans le milieu professionnel, Docker garantit que + l'application s'exécute de manière strictement identique sur les + machines de développement des développeurs et sur le serveur de + production, simplifiant ainsi drastiquement la configuration de + l'infrastructure. +\item + \textbf{Nginx \& Gunicorn} : Pour le déploiement en production, + \textbf{Gunicorn} fait office de serveur d'application WSGI, chargé + d'exécuter le code Python/Django en gérant efficacement les requêtes + concurrentes via un système de processus de travail (\emph{workers}). + Il est placé derrière \textbf{Nginx}, configuré comme un proxy inverse + (\emph{reverse proxy}). Nginx assure la sécurité globale, gère les + certificats SSL/TLS (HTTPS), sert directement les fichiers statiques + et médias lourds (comme les fichiers STL stockés), et distribue la + charge vers l'application backend. +\end{itemize} + +\hypertarget{outils-de-duxe9veloppement-et-de-collaboration}{% +\subsubsection{Outils de développement et de +collaboration}\label{outils-de-duxe9veloppement-et-de-collaboration}} + +\begin{itemize} +\tightlist +\item + \textbf{Visual Studio Code (VS Code)} : Choisi comme environnement de + développement intégré (IDE) principal par l'équipe. Il offre une + intégration native de Git, et des extensions pour Python/Django et + Angular. +\item + \textbf{Git \& GitFlow} : Pour la gestion de version, nous utilisons + \textbf{Git} hébergé sur \textbf{GitHub}. Afin d'organiser le travail + collaboratif de l'équipe sans bloquer la production, nous appliquons + la méthodologie \textbf{GitFlow}. Cette approche structure notre dépôt + en plusieurs branches strictes : \texttt{main} pour les versions + stables, \texttt{develop} pour centraliser les fonctionnalités prêtes, + et des branches \texttt{feature/} isolées pour chaque développeur. +\item + \textbf{Discord \& Webhooks GitHub} : Discord est notre outil central + de communication d'équipe. Pour automatiser notre suivi de projet, + nous y avons configuré des \textbf{webhooks GitHub}. À chaque fois + qu'un développeur pousse du code (\emph{push}), ouvre une demande de + fusion (\emph{pull request}) , une notification automatique est + envoyée dans un salon dédié sur Discord. Cela permet un suivi de + l'avancement facile. +\end{itemize} + +\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center} + +\newpage + +\hypertarget{ruxe9alisation}{% +\subsection{4.2. Réalisation}\label{ruxe9alisation}} + +Pour la conteneurisation, il a fallu créer les conteneurs. Pour se +faire, il faut configurer un fichier ``docker-compose.yaml'' afin qu'il +créer un conteneur PostgreSQL configuré sur le bon port après la +commande. + +Enfin pour la mise en commun du code, on a utilisé Git, et plus +particulièrement git flow qui permet de produire simplement un cadre de +travail professionnel (avec les branches main, develop, +features/\ldots{} etc.) et des commandes qui facilitent son utilisation. + +Afin de valider l'expérience utilisateur et de documenter le rendu final +de notre application PrIntech, cette section présente les principales +interfaces développées. Les captures d'écran sont structurées selon les +deux grands espaces de la plateforme : l'espace adhérent (client) et +l'espace d'administration (opérateur). + +\hypertarget{espace-utilisateur-et-authentification}{% +\subsubsection{4.2.1. Espace Utilisateur et +Authentification}\label{espace-utilisateur-et-authentification}} + +Cet espace regroupe les écrans accessibles par les étudiants du campus +pour gérer leur compte et soumettre leurs impressions. + +\begin{itemize} +\item + \textbf{Interface de Connexion} + \includegraphics{screenshots/login.png}\\ + Cette interface épurée permet l'authentification sécurisée des + utilisateurs (Fonctionnalité \textbf{F01}). +\item + \textbf{Page d'Accueil} \includegraphics{screenshots/home.png}\\ + Le tableau de bord principal de l'étudiant. Il offre une vue + d'ensemble claire sur ses activités courantes, son solde mis en valeur + (Fonctionnalité \textbf{F03-6 : Crédit restant}). +\item + \textbf{Dépôt d'une demande d'impression} + \includegraphics{screenshots/request.png}\\ + Formulaire intuitif dédié au dépôt des fichiers 3D (Fonctionnalité + \textbf{F03-1 : Déposer un STL}). L'étudiant peut y téléverser son + modèle, spécifier la quantité d'exemplaires souhaitée (\textbf{F03-7}) + ainsi que le choix des matériaux avant l'envoi au serveur. +\item + \textbf{Historique des travaux} + \includegraphics{screenshots/historique.png}\\ + Cette vue liste l'intégralité des commandes passées par l'étudiant. + (\textbf{F03-4 : Position dans la file}). +\end{itemize} + +\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center} + +\hypertarget{espace-administration-et-gestion-des-opuxe9rations}{% +\subsubsection{4.2.2. Espace Administration et Gestion des +Opérations}\label{espace-administration-et-gestion-des-opuxe9rations}} + +Réservé aux membres du bureau et aux opérateurs de l'association INTech +(Fonctionnalité \textbf{F04}), cet espace permet un pilotage complet du +service et du parc matériel. + +\begin{itemize} +\item + \textbf{Gestion de la file des travaux} + \includegraphics{screenshots/admin-requests.png}\\ + Interface maîtresse de l'opérateur (Fonctionnalité \textbf{F04-2 : + Gestion des travaux}). Elle centralise les fichiers \texttt{.stl} + soumis, permet d'assigner manuellement ou automatiquement une + imprimante libre et d'appliquer la tarification calculée. +\item + \textbf{Suivi du parc d'imprimantes} + \includegraphics{screenshots/admin-printers.png}\\ + Écran de supervision du statut des machines connectées. + L'administrateur peut visualiser instantanément quelles machines sont + disponibles (\texttt{UP}), occupées (\texttt{USED}) ou hors service + (\texttt{DOWN}). +\item + \textbf{Gestion des stocks de consommables} + \includegraphics{screenshots/admin-filaments.png}\\ + Vue dédiée au contrôle des stocks de plastique (Fonctionnalité + \textbf{F07 : Gestion des filaments}). Elle permet d'enregistrer les + nouvelles bobines, de surveiller la quantité de matière restante en + grammes (\textbf{F07-2}). +\item + \textbf{Suivi financier et des transactions} + \includegraphics{screenshots/admin-operations.png}\\ + Historique financier de la plateforme (Fonctionnalité \textbf{F04-4 : + Gestion crédit}). Cet écran liste de manière chronologique tous les + débits, les rechargements HelloAsso fructueux ainsi que les + remboursements automatiques émis suite à une erreur d'impression. +\item + \textbf{Administration des comptes utilisateurs} + \includegraphics{screenshots/admin-users.png}\\ + Interface de modération des profils (Fonctionnalité \textbf{F04-1}). + Elle permet également de modifier les rôles (adhérents, bureau, membre + projet) (\textbf{F03-5}). --- +\end{itemize} + +\newpage + +\hypertarget{validation-et-tests}{% +\section{5. Validation et tests}\label{validation-et-tests}} + +Pour garantir la robustesse de l'application PrINTech, nous avons mis en +place une stratégie de tests automatisés. L'effort principal a été +concentré sur la validation fonctionnelle du backend (Django), complété +par des mécanismes de tests d'intégration et d'interface (Playwright). + +\hypertarget{tests-backend-django}{% +\subsection{5.1. Tests Backend (Django)}\label{tests-backend-django}} + +Le backend constituant le cœur logique et financier de notre application +(notamment avec la gestion du crédit des utilisateurs), nous avons conçu +une suite de nombreux tests unitaires et d'intégration rigoureuse à +l'aide du framework de test de \emph{Django REST Framework} +(\texttt{APITestCase}). Les tests se trouvent dans le fichier +\texttt{PrINTech-Back/back/apps/api/tests.py} + +\hypertarget{exemples}{% +\subsubsection{5.1.1. Exemples:}\label{exemples}} + +Une attention particulière a été portée à la sécurité et à l'étanchéité +des droits entre les utilisateurs réguliers et les administrateurs du +bureau INTech. \textbf{Accès aux ressources critiques} : Nous testons +systématiquement qu'un utilisateur standard reçoit une erreur +\texttt{HTTP\_403\_FORBIDDEN} lorsqu'il tente de modifier l'état d'une +imprimante (endpoint \texttt{admin-detail}), alors qu'un jeton +d'administrateur valide la requête avec un statut +\texttt{HTTP\_200\_OK}. + +\textbf{Calcul automatique de la consommation} : Les tests valident que +l'API calcule correctement le poids total de plastique consommé en +multipliant le nombre d'impressions demandées par le poids unitaire de +la pièce. + +\textbf{Débit du solde utilisateur} : Nous nous assurons qu'à la +soumission d'un fichier STL valide, le crédit de l'étudiant est +immédiatement débité au prorata de la quantité et du type de filament +choisi (ex: passage d'une balance initiale de 100 unités à une valeur +calculée résiduelle). + +\textbf{Isolation des requêtes} : Nos cas de tests +(\texttt{test\_user\_can\_only\_see\_their\_own\_requests}) certifient +qu'un utilisateur connecté ne peut en aucun cas interroger ou lister les +fichiers et demandes d'un autre étudiant, prévenant ainsi toute fuite de +données. + +\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center} + +\hypertarget{tests-dinterface-playwright-et-validation-frontend}{% +\subsection{5.2. Tests d'Interface (Playwright) et Validation +Frontend}\label{tests-dinterface-playwright-et-validation-frontend}} + +Pour la partie émergée de l'iceberg (le Frontend), l'objectif initial +intégrait le déploiement de tests de bout en bout (E2E) via l'outil +\textbf{Playwright}. + +Cependant, comme évoqué dans notre bilan de projet (Section 6.1.3), le +retard accumulé sur l'apprentissage d'Angular et la complexité de +l'interfaçage asynchrone ont limité la couverture finale de ces tests +E2E. + +\newpage + +\hypertarget{bilan-du-projet}{% +\section{6. Bilan du projet}\label{bilan-du-projet}} + +\hypertarget{plannings}{% +\subsection{6.1. Plannings}\label{plannings}} + +On a eu de nombreux retards sur le projet, notamment à cause de la +participation de certains membres de notre équipe à la coupe de France +de Robotique dans le cadre du GATE. On a également sous-estimé la +difficulté d'apprentissage des technologies utilisés (Angular et +Django). On était pas tous capable de faire à la fois du frontend et du +backend ce qui a crée des complications. De plus Arnaud qui était censé +implémenté la communication avec les imprimantes n'a pas réussi ce qui a +fortement limité le projet. + +\hypertarget{planning-pruxe9visionnel}{% +\subsubsection{6.1.1. Planning +prévisionnel}\label{planning-pruxe9visionnel}} + +\begin{figure} +\centering +\includegraphics{planning-previsionnel.pdf} +\caption{Planning prévisionnel du projet PrIntech} +\end{figure} + +\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center} + +\newpage + +\hypertarget{planning-final}{% +\subsubsection{6.1.2. Planning final}\label{planning-final}} + +\begin{figure} +\centering +\includegraphics{planning-final.pdf} +\caption{Planning final du projet PrIntech} +\end{figure} + +\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center} + +\newpage + +\hypertarget{commentaires}{% +\subsubsection{6.1.3. Commentaires}\label{commentaires}} + +\begin{itemize} +\item + \textbf{Choix du planning prévisionnel} : + + \begin{itemize} + \tightlist + \item + Angular est appris en parallele du projet. Le dev front ne commence + qu'en fevrier. + \item + Django est maitrise -\textgreater{} execution rapide. La partie + Moonraker/Klipper est la plus incertaine. + \item + la phase 3 est la plus longue a cause de l'apprentissage en cours. + \end{itemize} +\item + \textbf{Comparaison au planning réel} : + + \begin{itemize} + \tightlist + \item + le projet a accumulé du retard notamment en raison d'autres dates + importantes comme les projets Gate ou bien les partiels mais aussi + car on a sous-estimé la complexité de certaines taches que nous + verrons plus bas. + \item + en réponse, on a ré-évalué le périmètre de notre projet afin d'en + inclure à la date du rendu que les fonctionnalités nécessaires au vu + du cahier des charges (Pas de tests e2e pour l'instant) + \item + Ce retard n'est pas une fatalité puisque compte tenu de l'utilité du + projet, celui-ci sera poursuivi à bien au-delà de la date de rendu. + \end{itemize} +\end{itemize} + +\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center} + +\newpage + +\hypertarget{plan-de-charges}{% +\subsection{6.2. Plan de charges}\label{plan-de-charges}} + +L'écart le plus marquant entre notre planification et la réalité réside +dans la courbe d'apprentissage des technologies choisies (Angular et +Django), qui s'est révélée bien plus longue et abrupte que prévu. +N'ayant pas de compétences approfondies préalables sur ces frameworks, +l'assimilation de concepts a pris plus de temps que prévu. + +\hypertarget{plan-de-charges-pruxe9visionnel-et-final}{% +\subsubsection{6.2.1. Plan de charges prévisionnel et +final}\label{plan-de-charges-pruxe9visionnel-et-final}} + +\begin{figure} +\centering +\includegraphics{planChargeSuiviActivites.png} +\caption{Plan de Charge} +\end{figure} + +\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center} + +\newpage + +\hypertarget{commentaires-1}{% +\subsubsection{6.2.3. Commentaires}\label{commentaires-1}} + +Nous avons bien utilisé le temps imparti. Celui-ci nous a permis de nous +former non seulement sur les technologies auxquelles nous étions +assignées mais aussi à comprendre l'ensemble du code que ce soit le +front ou le back. + +Nous sommes ainsi parvenu à rendre notre profil beaucoup plus attractif +avec une compréhension plus fine des abstractions des différentes +technologies mais aussi une vision beaucoup moins étroite de notre champ +d'action. + +Pour indication, Célian ne connaissait pas Django au démarrage du +projet, Adam démarrait sur Angular et Yanis était surtout focalisé sur +Django; maintenant, nous sommes tous en mesure de comprendre le code de +l'autre et de le corriger à travers un système de PR (Pull Request) sur +Git. + +\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center} + +\newpage + +\hypertarget{difficultuxe9s-rencontruxe9es}{% +\subsection{6.3. Difficultés +rencontrées}\label{difficultuxe9s-rencontruxe9es}} + +Notre projet, malgré un produit final assez satisfaisant, a été le +théâtres de certaines difficultés qui nous ont poussé à modifier notre +champ d'action : + +\begin{itemize} +\item + La mise en commun du travail a été plus nébuleuse que prévu (notamment + après l'instauration de PR qui ont empêchés le bon fonctionnement de + certaines commandes de git flow) +\item + les retards accumulés qui on sappé l'efficacité du projet malgré un + démarrage rapide +\item + l'utilisation des APIs des imprimantes 3D : Notre objectif initial + était de relier directement les imprimantes 3D à l'application grâce + au logiciel FDM Monster. Cependant, cette configuration s'est révélée + difficile en raison des systèmes propriétaires des machines. De plus, + comme les imprimantes sont très sollicitées, nous avons temporairement + configuré le site pour qu'un administrateur lance chaque impression + manuellement. Nous continuons néanmoins à travailler sur + l'automatisation de ce processus. +\end{itemize} + +\hypertarget{fonctionalituxe9s-ruxe9alisuxe9-ou-pas}{% +\subsubsection{6.3.1 Fonctionalités réalisé ou +pas}\label{fonctionalituxe9s-ruxe9alisuxe9-ou-pas}} + +\begin{longtable}[]{@{} + >{\raggedright\arraybackslash}p{(\columnwidth - 8\tabcolsep) * \real{0.1818}} + >{\raggedright\arraybackslash}p{(\columnwidth - 8\tabcolsep) * \real{0.1818}} + >{\raggedright\arraybackslash}p{(\columnwidth - 8\tabcolsep) * \real{0.1818}} + >{\centering\arraybackslash}p{(\columnwidth - 8\tabcolsep) * \real{0.2273}} + >{\centering\arraybackslash}p{(\columnwidth - 8\tabcolsep) * \real{0.2273}}@{}} +\toprule\noalign{} +\begin{minipage}[b]{\linewidth}\raggedright +ID +\end{minipage} & \begin{minipage}[b]{\linewidth}\raggedright +Fonctionnalité de l'application +\end{minipage} & \begin{minipage}[b]{\linewidth}\raggedright +Description technique +\end{minipage} & \begin{minipage}[b]{\linewidth}\centering +Priorité +\end{minipage} & \begin{minipage}[b]{\linewidth}\centering +Statut +\end{minipage} \\ +\midrule\noalign{} +\endhead +\bottomrule\noalign{} +\endlastfoot +\textbf{F01} & \textbf{Authentification} & Connexion et déconnexion. & +Haute & \textbf{Réalisé} \\ +\textbf{F02} & \textbf{Gestion des profils} & Affichage et modification +des informations utilisateurs. & Basse & \textbf{Réalisé} \\ +\textbf{F03} & \textbf{Gestion des impressions} & Flux complet +d'impression 3D, du dépôt STL à la mise en file. & Haute & +\textbf{Réalisé} \\ +\textbf{F03-1} & \textbf{Déposer un STL} & Upload d'un fichier STL. & +Haute & \textbf{Réalisé} \\ +\textbf{F03-2} & \textbf{Slicer automatique} & Convertir le modèle en +G-code via un slicer. & Moyenne & \textbf{Non réalisé} \\ +\textbf{F03-3} & \textbf{Estimation du coût} & Calculer le prix +d'impression à partir des paramètres. & Basse & \textbf{Non réalisé} \\ +\textbf{F03-4} & \textbf{Position dans la file} & Afficher la place du +travail dans la file. & Basse & \textbf{Non réalisé} \\ +\textbf{F03-5} & \textbf{Priorité projets INTech} & Rôle dédié pour +prioriser les impressions. & Basse & \textbf{Réalisé} \\ +\textbf{F03-6} & \textbf{Crédit restant} & Afficher le crédit restant +d'un utilisateur. & Basse & \textbf{Réalisé} \\ +\textbf{F03-7} & \textbf{Quantité d'impressions} & Indiquer le nombre +d'exemplaires à imprimer. & Basse & \textbf{Réalisé} \\ +\textbf{F04} & \textbf{Espace administrateur} & Interface +d'administration pour une gestion complète. & Moyenne & +\textbf{Réalisé} \\ +\textbf{F04-1} & \textbf{Gestion des utilisateurs} & Créer, modifier et +supprimer les utilisateurs. & Haute & \textbf{Réalisé} \\ +\textbf{F04-2} & \textbf{Gestion des travaux} & Créer, modifier et +supprimer la file de travaux. & Moyenne & \textbf{Réalisé} \\ +\textbf{F04-3} & \textbf{Reprise des erreurs} & Déclarer un travail en +erreur et le remettre en file. & Moyenne & \textbf{Non réalisé} \\ +\textbf{F04-4} & \textbf{Gestion crédit} & Gestion des crédits des +utilisateurs (recharge). & Haute & \textbf{Réalisé} \\ +\textbf{F05} & \textbf{Intégrations} & SSO et notifications externes. & +Optionnelle & \textbf{Non réalisé} \\ +\textbf{F05-1} & \textbf{Intégration CAS} & Inscription et connexion via +le CAS de l'école. & Optionnelle & \textbf{Non réalisé} \\ +\textbf{F05-2} & \textbf{Intégration Discord} & Utiliser des webhooks +pour les notifications. & Optionnelle & \textbf{Non réalisé} \\ +\textbf{F05-3} & \textbf{Intégration HelloAsso} & Créditer les comptes +via HelloAsso. & Optionnelle & \textbf{Non réalisé} \\ +\textbf{F06} & \textbf{Impression technique} & Options avancées +d'impression. & Optionnelle & \textbf{Non réalisé} \\ +\textbf{F06-1} & \textbf{Utilisation de supports} & Détecter la +nécessité de supports. & Optionnelle & \textbf{Non réalisé} \\ +\textbf{F06-2} & \textbf{Paramétrage du slicer} & Permettre l'upload +d'un JSON de paramètres. & Optionnelle & \textbf{Non réalisé} \\ +\textbf{F06-3} & \textbf{Choix des matériaux} & Choisir un plastique +autre que le PLA. & Optionnelle & \textbf{Réalisé} \\ +\textbf{F07} & \textbf{Gestion des filaments} & Administration des +filaments et du stock. & Optionnelle & \textbf{Réalisé} \\ +\textbf{F07-1} & \textbf{Activation des filaments} & Activer et +désactiver des filaments. & Optionnelle & \textbf{Non réalisé} \\ +\textbf{F07-2} & \textbf{Gestion du filament} & Gérer la quantité de +filament restant. & Optionnelle & \textbf{Réalisé} \\ +\textbf{F07-3} & \textbf{Gestion de consommation} & Rapport sur +quantités, crédits et montant. & Optionnelle & \textbf{réalisé} \\ +\end{longtable} + +\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center} + +\newpage + +\hypertarget{conclusion-et-perspective}{% +\section{7. Conclusion et perspective}\label{conclusion-et-perspective}} + +Malgré des difficultés, notre projet a produit une Application Web +fonctionnelle selon les critères que nous nous étions imposés en +préambule avec le cahier des charges. + +Tout du moins, nous souhaiterions peaufiner ce projet au-delà du rendu +final, que ce soit par la continuation de celui-ci par un autre groupe +de projet informatique, ou bien par la maintenance de celui-ci par +l'Association Intech elle-même. Il faudra donc le déployer ce qu'on a +pas encore pu faire. On pense utilisé une machine virtuelle fournie par +l'association Minet. À terme il pourra être réellement utilisé par +l'association. + +Ce projet est un projet enrichissant pour un étudiant ingénieur dans le +développement informatique à travers l'utilisation d'outils et de +technologies professionnels qui s'intègre pleinement dans le cadre de +nos études et du campus avec Intech. + +Ce projet, au-delà de la réalisation purement technique, est aussi une +expérience d'équipe avec tout ses avantages et ses défauts qu'on a +expérimenté (comme les retards et la mise en commun du code). Cela en +fait donc une force pour tous nos futurs projets en groupe. + +\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center} diff --git a/Livrable/plan-de-charge-previsionnel.pdf b/Livrable/plan-de-charge-previsionnel.pdf new file mode 100644 index 0000000..0a979ad Binary files /dev/null and b/Livrable/plan-de-charge-previsionnel.pdf differ diff --git a/Livrable/planChargeSuiviActivites.ods b/Livrable/planChargeSuiviActivites.ods new file mode 100644 index 0000000..89e8d63 Binary files /dev/null and b/Livrable/planChargeSuiviActivites.ods differ diff --git a/Livrable/planChargeSuiviActivites.pdf b/Livrable/planChargeSuiviActivites.pdf new file mode 100644 index 0000000..a6cc0d5 Binary files /dev/null and b/Livrable/planChargeSuiviActivites.pdf differ diff --git a/Livrable/planChargeSuiviActivites.png b/Livrable/planChargeSuiviActivites.png new file mode 100644 index 0000000..ff5d070 Binary files /dev/null and b/Livrable/planChargeSuiviActivites.png differ diff --git a/Livrable/planning-final.pdf b/Livrable/planning-final.pdf new file mode 100644 index 0000000..8978945 Binary files /dev/null and b/Livrable/planning-final.pdf differ diff --git a/Livrable/planning-previsionnel.pdf b/Livrable/planning-previsionnel.pdf new file mode 100644 index 0000000..e391978 Binary files /dev/null and b/Livrable/planning-previsionnel.pdf differ diff --git a/Livrable/rapport-final.md b/Livrable/rapport-final.md new file mode 100644 index 0000000..82cd0f1 --- /dev/null +++ b/Livrable/rapport-final.md @@ -0,0 +1,430 @@ +--- +title: "Rapport de Projet : PrINTech" +subtitle: "Encadrant : Mohamed SELLAMI | Version : 3.0" +subject: "Livrable 3" +author: | + `\begin{tabular}{cc} + Adam ALLOU & Arnaud COSTERMANS \\ + \footnotesize adam.allou@telecom-sudparis.eu & \footnotesize arnaud.costermans@telecom-sudparis.eu \\ + & \\ + Célian GIAUME & Yanis BELLOT \\ + \footnotesize celian.giaume@telecom-sudparis.eu & \footnotesize yanis.bellot@telecom-sudparis.eu + \end{tabular}`{=latex} +date: "06 Juin 2026" +toc: true +toc-title: "Sommaire du Rapport Final" + +--- + +\newpage + +# 1. Introduction + +Depuis cette année, l'association INTech du campus rencontre des difficultés pour gérer les impressions 3D réalisées par ses membres. En effet, INTech offre désormais aux étudiants de l'école la possibilité d'imprimer leurs modèles 3D en devenant adhérents, selon le processus décrit dans la figure 1. + +Le processus commence lorsqu'un étudiant trouve un modèle 3D, renomme le fichier à son nom (au format `NOM-PRENOM.stl`), puis remplit un Google Form pour s'enregistrer dans un tableur avant de lancer l'impression. Si l'étudiant n'est pas présent à la fin du travail et que l'imprimante est requise pour une autre tâche, un opérateur de l'association retire la pièce et la stocke au local en attendant son retour. À la fin du mois, la phase de facturation s'enclenche : l'opérateur consulte le tableur Google Sheets pour calculer le montant total dû, crée un lien de paiement sur **HelloAsso** et envoie la facture à l'étudiant. Si ce dernier règle sa facture sous 7 jours, l'opérateur valide la transaction dans sa comptabilité, ce qui marque le succès du processus. En revanche, s'il ne règle pas sa dette après plusieurs relances hebdomadaires et que le plafond maximal d'avertissements est atteint, l'étudiant est inscrit sur liste noire, ce qui entraîne l'échec de la procédure. + +Actuellement, ce système de gestion présente plusieurs limites. Les adhérents doivent télécharger et paramétrer un logiciel de découpe (*slicer*) qui permet de transformer un modèle 3D en une série d'instructions pour les imprimantes, appelée G-code. On demande ensuite aux adhérents de charger ce G-code sur les imprimantes, de lancer les impressions, puis de remplir un Google Form en y renseignant le nom de leur fichier, le poids de l'impression et d'autres paramètres techniques. + +![Diagramme BPMN du fonctionnement pré-PrIntech](diagrams/bpmn-before-diagram.svg) + + +Ce fonctionnement implique de nombreuses étapes manuelles susceptibles d'engendrer des erreurs et de faciliter la fraude. De plus, il impose de former chaque nouvel adhérent à l'utilisation physique des machines. + +C'est dans ce contexte qu'intervient le projet PrINTech, qui consiste à développer une application web dynamique afin d'automatiser la gestion des impressions 3D. Contrairement à la solution existante, ce projet s'inscrit dans une vision à long terme : il facilite le passage à l'échelle (intégration de nouvelles imprimantes ou augmentation du nombre d'utilisateurs) et permet de réduire considérablement la charge de travail du bureau de l'association. + +--- + +\newpage +## 1.1. Terminologie + +Pour faciliter la lecture de ce rapport, cette section regroupe et définit les termes techniques, les acronymes et les notions spécifiques au domaine de l'impression 3D et du développement web abordés dans ce document. + +### Impression 3D et Matériaux +* **Fichier STL (`.stl`)** : Format de fichier standard en impression 3D (Stéréolithographie) représentant la géométrie de la surface d'un objet 3D sous forme de triangles. +* **Slicer (Trancheur)** : Logiciel qui découpe un modèle 3D virtuel en fines couches horizontales et génère les instructions nécessaires à l'imprimante. +* **G-code** : Langage de programmation numérique universel utilisé pour piloter les machines-outils et les imprimantes 3D. +* **Filament** : Fil de plastique en bobine servant de matière première à l'imprimante 3D pour fabriquer les objets. +* **PETG** : Type de plastique très résistant (similaire à celui des bouteilles d'eau, mais modifié), souvent utilisé pour des pièces devant supporter des chocs ou une exposition extérieure. +* **PLA (Acide Polylactique)** : Plastique d'origine végétale couramment utilisé en impression 3D. +* **Supports** : Structures temporaires imprimées en même temps que le modèle pour soutenir les parties dans le vide. + +### Technologies Web & Architecture +* **Angular** : Framework côté client (Frontend) utilisé pour concevoir une interface utilisateur dynamique. +* **Django** : Framework de haut niveau basé sur Python, utilisé pour le développement de la logique métier (Backend). +* **API REST** : Style d'architecture logicielle permettant au Frontend et au Backend de communiquer via le protocole HTTP. +* **PostgreSQL** : Système de gestion de base de données relationnelle open-source. +* **Docker & Conteneurisation** : Technologie permettant d'isoler une application et ses dépendances dans un environnement virtuel appelé "conteneur". +* **Nginx & Gunicorn** : Duo de production où Nginx sert de serveur proxy inverse et Gunicorn de serveur d'application WSGI pour exécuter le code Python. + +### Outils de Développement & Tests +* **Git / Git Flow** : Git est le système de contrôle de version ; Git Flow est la méthodologie qui structure la collaboration (branches `main`, `develop`, `features/`). +* **PR (Pull Request)** : Demande formelle de fusion de code soumise par un développeur pour relecture et validation. +* **uv** : Gestionnaire de paquets et d'environnements Python extrêmement rapide, écrit en Rust. +* **Playwright** : Framework de test de bout en bout (E2E) permettant de simuler le comportement d'un utilisateur réel. + +\newpage +# 2. Cahier des charges + +À la suite des rapports et des échanges menés avec le bureau et les membres actifs de l'association INTech, une analysis approfondie des besoins opérationnels a été réalisée. Le constat est simple : le volume croissant de demandes d'impression 3D sur le campus sature le modèle de gestion actuel, basé sur des processus manuels et des outils tiers (Google Forms, tableurs Sheets). + +## 2.1. Description du sujet + +Notre projet consiste en la réalisation d'une application web dynamique. Le site doit être capable d'interagir avec une base de données qu'il nous incombe d'implémenter. Cette plateforme permettra l'identification sécurisée des étudiants souhaitant imprimer des modèles 3D grâce à un système de gestion des utilisateurs, des requêtes et des files d'attente. + +Cette plateforme aura pour objectifs principaux : + +* **L'authentification sécurisée** des étudiants du campus souhaitant utiliser les services d'impression 3D. +* **La gestion des utilisateurs** en distinguant les rôles (adhérents, bureau, membres d'un projet) et leurs droits associés. +* **Le traitement des requêtes** afin de centraliser, suivre et historiser toutes les demandes de fabrication soumises par les élèves. +* **La gestion des crédits et de la tarification** afin de permettre le paiement de chaque impression. +* **Un espace administrateur** offrant une interface de gestion complète pour le bureau. + +\newpage + +## 2.2. Liste des fonctionnalités de l'application +Afin de répondre au sujet, l'application doit intégrer les fonctionnalités suivantes, classées par priorité de développement : + +| ID | Fonctionnalité de l'application | Description technique | Priorité | +| :--- |:--------------| :--------------------- | :---: | +| **F01** | **Authentification** | Connexion et déconnexion. | Haute | +| **F02** | **Gestion des profils** | Affichage et modification des informations utilisateurs. | Basse | +| **F03** | **Gestion des impressions** | Flux complet d'impression 3D, du dépôt STL à la mise en file. | Haute | +| **F03-1** | **Déposer un STL** | Upload d'un fichier STL. | Haute | +| **F03-2** | **Slicer automatique** | Convertir le modèle en G-code via un slicer. | Moyenne | +| **F03-3** | **Estimation du coût** | Calculer le prix d'impression à partir des paramètres. | Basse | +| **F03-4** | **Position dans la file** | Afficher la place du travail dans la file. | Basse | +| **F03-5** | **Priorité projets INTech** | Chaque utilisateur a un rôle (adhérent, bureau, membre d'un projet) pour prioriser les impressions. | Basse | +| **F03-6** | **Crédit restant** | Afficher le crédit restant d'un utilisateur. | Basse | +| **F03-7** | **Quantité d'impressions** | Indiquer le nombre d'exemplaires à imprimer. | Basse | +| **F04** | **Espace administrateur** | Interface d'administration pour une gestion complète. | Moyenne | +| **F04-1** | **Gestion des utilisateurs** | Créer, modifier et supprimer les utilisateurs. | Haute | +| **F04-2** | **Gestion des travaux** | Créer, modifier et supprimer la file de travaux. | Moyenne | +| **F04-3** | **Reprise des erreurs** | Déclarer un travail en erreur et le remettre en file. | Moyenne | +| **F04-4** | **Gestion crédit** | Gestion des crédits des utilisateurs, incluant la recharge. | Haute | +| **F05** | **Intégrations** | SSO et notifications externes. | Optionnelle | +| **F05-1** | **Intégration CAS** | Inscription et connexion via le CAS de l'école. | Optionnelle | +| **F05-2** | **Intégration Discord** | Utiliser des webhooks pour les notifications. | Optionnelle | +| **F05-3** | **Intégration HelloAsso** | Créditer les comptes via HelloAsso. | Optionnelle | +| **F06** | **Impression technique** | Options avancées d'impression. | Optionnelle | +| **F06-1** | **Utilisation de supports** | Détecter quand l'utilisation de supports est nécessaire. | Optionnelle | +| **F06-2** | **Paramétrage du slicer** | Permettre l'upload d'un JSON de paramètres. | Optionnelle | +| **F06-3** | **Choix des matériaux** | Choisir un plastique autre que le PLA. | Optionnelle | +| **F07** | **Gestion des filaments** | Administration des filaments et du stock. | Optionnelle | +| **F07-1** | **Activation des filaments** | Activer et désactiver des filaments. | Optionnelle | +| **F07-2** | **Gestion du filament** | Gérer la quantité de filament restant. | Optionnelle | +| **F07-3** | **Gestion de consommation** | Rapport sur les quantités, crédits et montants imprimés. | Optionnelle | + +--- + +\newpage + +# 3. Conception + +## 3.1. Base de données + +Comme l'illustre ce schéma, lorsqu'un utilisateur (caractérisé par un rôle applicatif et un solde de crédits) souhaite fabriquer un objet, il émet une demande d'impression (*Request*). Celle-ci encapsule un fichier numérique (*File*) contenant le modèle 3D et ses paramètres de découpage au format JSON (*para_slicer*), lui-même associé à un consommable précis (*Filament*) défini par son type (PLA ou PETG) et sa couleur. La demande est ensuite affectée à une machine (*Printer*) identifiée par son modèle et son état de disponibilité. Enfin, pour éradiquer les risques de fraude et automatiser la comptabilité, chaque transaction (recharge de compte par carte/espèces ou débit/remboursement lié à une impression) est tracée par l'entité *Operation*. Cette table lie un agent du bureau à un utilisateur bénéficiaire tout en restant historiquement connectée à la *Request* d'origine. + +![Schéma de base de données](diagrams/db-diagram.svg) + +--- + +\newpage + +## 3.2. Exemple de schéma: Séquence de l'Authentification + +Pour sécuriser l'accès aux ressources de la plateforme et garantir que chaque étudiant interagit uniquement avec ses propres données, le projet implémente un flux d'authentification basé sur des jetons d'accès (JWT) via la bibliothèque Django REST Framework SimpleJWT représenté dans le schéma ci-dessous. Ce mécanisme élimine les failles de sécurité de l'ancien système et permet une communication fluide entre le Frontend Angular et l'API Backend. + + +![Schéma de l'authentification](diagrams/auth-diagram.svg) + +--- + +\newpage + +## 3.3. Processus d'impression 3D avec PrIntech + +### Description du processus d'impression 3D + +Le processus débute lorsqu'un étudiant dépose un modèle 3D sur la plateforme (**F03-1 : Déposer un STL**). Une fois la requête enregistrée dans l'interface d'administration (**F04-2 : Gestion des travaux**), le système évalue automatiquement les ressources nécessaires. Il vérifie d'une part la quantité de matière disponible (**F07-2 : Gestion du filament**) et d'autre part le montant estimé de l'impression (**F03-3 : Estimation du coût**) par rapport aux ressources financières de l'utilisateur (**F03-6 : Crédit restant**). Si le plastique est manquant ou indisponible, la demande est suspendue. Si l'utilisateur ne dispose pas de fonds suffisants, l'application le redirige vers le module externe afin de renflouer son compte (**F05-3 : Intégration HelloAsso**). + +Dès que ces vérifications sont validées, le travail rejoint la file d'attente informatique et l'opérateur lui attribue une machine disponible (**F04-2 : Gestion des travaux**), tout en prenant en compte le rôle de l'utilisateur pour la gestion des priorités (**F03-5 : Priorité projets INTech**). L'application procède alors au prélèvement des fonds requis sur le compte de l'étudiant (**F04-4 : Gestion crédit**), met à jour le statut informatique de l'imprimante, et l'opérateur lance physiquement la fabrication de la pièce (**F03 : Gestion des impressions**). À la fin du cycle, deux situations peuvent se présenter : +* **En cas de succès :** l'opérateur valide la fin de la tâche (**F04-2 : Gestion des travaux**), l'imprimante repasse informatiquement à l'état disponible, le stock de plastique restant est ajusté (**F07-2 : Gestion du filament**) et une alerte automatisée est envoyée à l'étudiant (**F05-2 : Intégration Discord**) pour l'inviter à récupérer sa pièce. +* **En cas d'échec :** l'opérateur déclare l'avarie sur l'interface (**F04-3 : Reprise des erreurs**), libérant immédiatement la machine. Le système réajuste alors automatiquement le solde de l'étudiant (**F04-4 : Gestion crédit**) tout en envoyant une notification Discord pour l'avertir du problème (**F05-2 : Intégration Discord**). + +![Diagramme BPMN du fonctionnement post-PrIntech](diagrams/bpmn-after-diagram.svg) + +--- + +\newpage + +# 4. Réalisation et choix techniques + +## 4.1. Choix des technologies + +Les technologies disponibles pour développer une application web sont nombreuses. Pour effectuer notre sélection, nous avons d'abord examiné les outils capables de répondre à notre cahier des charges, avant de nous appuyer sur les technologies avec lesquelles les membres de notre équipe avaient déjà de l'expérience. Sur la base de ces critères, nous avons sélectionné la pile technique (*stack*) suivante. + +Pour le frontend : +- **Angular** : Pour la réalisation de l'interface utilisateur. Angular s'appuie sur une architecture en composants et sur le langage **TypeScript**, apportant un typage fort et une structure rigoureuse. + +Pour le backend : +- **Django (Python)** : Utilisé pour développer le cœur de la logique métier et l'API REST (via *Django REST Framework*). +- **uv** : Afin de gérer nos bibliothèques et nos versions de Python, nous avons utilisé `uv`. C'est un équivalent de Poetry, mais optimisé car implémenté en Rust. Il est nécessaire de préfixer nos commandes par `uv run` afin que le processus s'exécute dans l'environnement dédié. +- **Swagger (OpenAPI)** : Intégré à notre backend pour la documentation et le test de l'API REST. Swagger génère automatiquement une interface web interactive à partir des routes Django. Cela permet aux développeurs frontend de comprendre instantanément les points de terminaison (*endpoints*), les paramètres attendus et les réponses de l'API, facilitant grandement la communication et l'intégration. +- **PostgreSQL** : Ce système de gestion de base de données relationnelle (SGBDR) open-source a été retenu pour sa robustesse, sa conformité ACID et sa gestion fine des transactions financières (crédits, remboursements, opérations). Sa capacité à indexer efficacement les données structurées garantit des performances optimales lors de la montée en charge du système et du suivi des files d'attente. +- **Klipper** : Firmware communautaire utilisé pour remplacer le micrologiciel constructeur des imprimantes. +- **Moonraker** : Outil qui expose les API JSON-RPC de Klipper sous forme d'API REST. +- **FDM Monster** : Cet orchestrateur open-source centralise la gestion d'une flotte d'imprimantes 3D. Connecté aux différentes instances Moonraker, il permet de piloter le parc, de centraliser le contrôle des machines et de gérer globalement les files d'attente d'impression. + +### Tests, Validation et Qualité Logicielle +- **Tests natifs Django (TestCase)** : Intégrés directement au backend, le framework de test unitaire et d'intégration natif de Django permet de sécuriser l'intégrité de la base de données lors des transactions critiques. Ils valident de manière isolée les calculs de prix, l'exactitude des débits et remboursements, le respect des rôles pour la gestion des priorités, ainsi que les transitions de statut des requêtes d'impression (du dépôt initial jusqu'à la mise en file). +- **Playwright** : Framework moderne retenu pour l'automatisation des tests de bout en bout (*End-to-End*). Complémentaire aux tests Django, Playwright nous permet de simuler le parcours complet d'un utilisateur sur un navigateur réel de manière isolée. + +### Conteneurisation et Infrastructure +- **Docker & Docker Compose** : Afin de partitionner logiquement les différentes couches applicatives (Frontend, Backend, Base de données), nous avons conteneurisé chaque service. Technologie incontournable en milieu professionnel, Docker garantit que l'application s'exécute de manière strictement identique sur les machines de développement et sur le serveur de production, simplifiant ainsi la configuration de l'infrastructure. +- **Nginx & Gunicorn** : Pour le déploiement en production, **Gunicorn** fait office de serveur d'application WSGI, chargé d'exécuter le code Python/Django en gérant efficacement les requêtes concurrentes via un système de processus de travail (*workers*). Il est placé derrière **Nginx**, configuré comme un proxy inverse (*reverse proxy*). Nginx assure la sécurité globale, gère les certificats SSL/TLS (HTTPS), sert directement les fichiers statiques ou médias lourds (comme les fichiers STL stockés) et distribue la charge vers le backend. + +### Outils de développement et de collaboration +* **Visual Studio Code (VS Code)** : Choisi comme environnement de développement intégré (IDE) principal par l'équipe. +* **Git & GitFlow** : Pour la gestion de version, nous utilisons **Git** hébergé sur **GitHub**. Afin d'organiser le travail collaboratif sans bloquer la production, nous appliquons la méthodologie **GitFlow** (branches `main`, `develop`, et `feature/` isolées). +* **Discord & Webhooks GitHub** : Discord est notre outil de communication principal. À chaque fois qu'un développeur pousse du code (*push*) ou ouvre une demande de fusion (*pull request*), une notification automatique est envoyée dans un salon dédié sur Discord, facilitant le suivi de l'avancement. + +--- + +\newpage + +## 4.2. Réalisation + +Pour la conteneurisation, il a fallu configurer un fichier `docker-compose.yaml` afin de déployer un conteneur PostgreSQL configuré sur le bon port. Enfin, pour la mise en commun du code, l'utilisation de Git Flow a permis de définir un cadre de travail professionnel clair. + +Afin de valider l'expérience utilisateur et de documenter le rendu final de notre application PrINTech, cette section presents les principales interfaces développées, divisées entre l'espace adhérent (client) et l'espace d'administration (opérateur). + +\newpage + +### 4.2.1. Espace Étudiant et Demandes d'Impression + +* **Interface de Connexion** : Cette interface épurée permet l'authentification sécurisée des utilisateurs (Fonctionnalité **F01**). + ![Interface de connexion de l'application PrINTech](screenshots/login.png) + +* **Page d'Accueil** : Le tableau de bord principal de l'étudiant. Il offre une vue d'ensemble claire sur ses activités courantes et met en valeur son solde (Fonctionnalité **F03-6 : Crédit restant**). + ![Page d'accueil de l'espace utilisateur](screenshots/home.png) + +* **Dépôt d'une demande d'impression** : Formulaire intuitif dédié au dépôt des fichiers 3D (Fonctionnalité **F03-1 : Déposer un STL**). L'étudiant peut y téléverser son modèle, spécifier la quantité d'exemplaires souhaitée (**F03-7**) ainsi que le choix des matériaux avant l'envoi au serveur. + ![Interface de création d'une requête d'impression](screenshots/request.png) + +* **Historique des travaux** : Cette vue liste l'intégralité des commandes passées par l'étudiant reprenant la date de dépôt, le type de projet et leur statut en direct (Fonctionnalité **F03-4 : Position dans la file**). + ![Historique utilisateur](screenshots/historique.png) + +* **Profil** : Permet l'affichage et modification des informations utilisateurs: photo de profil, mot de passe (Fonctionnalité **F02 : Gestion des profils**). + ![Historique utilisateur](screenshots/profile.png) + +--- + +\newpage + +### 4.2.2. Espace Administration et Gestion des Opérations + +Réservé aux membres du bureau et aux opérateurs de l'association INTech (Fonctionnalité **F04**), cet espace permet un pilotage complet du service et du parc matériel. + +* **Gestion de la file des travaux** : Interface maîtresse de l'opérateur (Fonctionnalité **F04-2 : Gestion des travaux**). Elle centralise les fichiers `.stl` soumis, permet d'assigner manuellement ou automatiquement une imprimante libre et d'appliquer la tarification calculée. + ![Administration des requêtes](screenshots/admin-requests.png) + +\newpage + +* **Suivi du parc d'imprimantes** : Écran de supervision du statut des machines connectées. L'administrateur peut visualiser instantanément quelles machines sont disponibles (`UP`), occupées (`USED`) ou hors service (`DOWN`). + ![Gestion des imprimantes](screenshots/admin-printers.png) + +* **Gestion des stocks de consommables** : Vue dédiée au contrôle des stocks de plastique (Fonctionnalité **F07 : Gestion des filaments**). Elle permet d'enregistrer les nouvelles bobines et de surveiller la quantité de matière restante en grammes (**F07-2**). + ![Gestion des filaments](screenshots/admin-filaments.png) + +* **Suivi financier et des transactions** : Historique financier de la plateforme (Fonctionnalité **F04-4 : Gestion crédit**). Cet écran liste de manière chronologique tous les débits, les rechargements HelloAsso fructueux ainsi que les remboursements automatiques émis suite à une erreur d'impression. + ![Historique des opérations financières](screenshots/admin-operations.png) + +* **Administration des comptes utilisateurs** : Interface de modération des profils (Fonctionnalité **F04-1**). Elle permet également de modifier les rôles (adhérents, bureau, membres d'un projet) (**F03-5**). + ![Gestion des utilisateurs](screenshots/admin-users.png) + + --- + +\newpage + +# 5. Validation et tests + + +Pour garantir la robustesse de l'application PrINTech, nous avons mis en place une stratégie de tests automatisés. L'effort principal a été concentré sur la validation fonctionnelle du backend (Django), complété par des mécanismes de tests d'intégration et d'interface (Playwright). + +## 5.1. Tests Backend (Django) + +Le backend constituant le cœur logique et financier de notre application, nous avons conçu une suite rigoureuse de tests unitaires et d'intégration à l'aide du framework de test de *Django REST Framework* (`APITestCase`). Les tests se trouvent dans le fichier `PrINTech-Back/back/apps/api/tests.py` et couvrent les fonctionnalités clés du système : + +### 5.1.1. Fonctionnalités validées par les tests automatiques : + +* **F01 (Authentification) & Sécurisation des routes** : + * Nous validons la génération et le rejet des requêtes selon la validité des jetons d'accès JWT (`test_missing_token`, `test_unauthenticated_cannot_upload`). + * L'étanchéité des droits est testée systématiquement : un utilisateur standard reçoit une erreur `HTTP_403_FORBIDDEN` lorsqu'il tente d'accéder aux routes d'administration (`test_regular_user_cannot_access_admin_user_endpoints`, `test_user_cannot_access_admin_endpoint`, `test_admin_access`). + +* **F02 (Gestion des profils)** : + * Les tests vérifient les contraintes de modification de profil, comme le changement sécurisé de mot de passe (`test_change_password_wrong_old_password`), ainsi que la gestion stricte de la photo de profil (`test_upload_profile_picture`, `test_delete_profile_picture_when_none_exists`). + * L'isolation des requêtes est certifiée par le test `test_no_access_other_user_request` : un utilisateur connecté ne peut en aucun cas interroger, modifier ou lister les demandes d'un autre étudiant, prévenant toute fuite de données. + +* **F03 (Gestion des impressions) & F03-7 (Quantité)** : + * Le flux complet de soumission est validé par `test_create_request`. + * Bien que les tests d'interface automatisés n'aient pas pu valider le calcul de consommation frontend, la logique backend est blindée : l'API calcule et valide le coût total ainsi que la cohérence des paramètres avant toute mise en file. + +* **F03-5 (Priorité projets INTech)** : + * Le test critique `test_admin_request_list_priority_sorting` certifie que l'API trie et ordonne automatiquement les demandes en attente selon la hiérarchie stricte des rôles : `BUREAU` (0) > `ROBOTECH` (1) > `ADHERENT` (2). + * Le test `test_default_role_is_adherent` assure qu'un nouvel utilisateur est assigné par défaut au rôle le plus restrictif. + +* **F03-6 (Crédit restant) & F04-4 (Gestion crédit)** : + * Les scénarios de paiement et de remboursement sont entièrement automatisés. `test_pay_request_success` valide le débit immédiat du crédit de l'étudiant au prorata du prix fixé, tandis que `test_cancel_request_with_refund` s'assure du recrédit automatique du solde en cas d'annulation. + * Les garde-fous contre la fraude sont vérifiés par `test_user_cannot_update_credit_manually` (un utilisateur ne peut pas modifier son solde lui-même) et `test_pay_request_insufficient_funds` (blocage de la transaction si le solde est insuffisant). + +* **F04-1 (Gestion des utilisateurs) & F04-2 (Gestion des travaux)** : + * Les privilèges des opérateurs du bureau sont testés pour la modification des rôles applicatifs (`test_admin_can_modify_user_role`) et le changement d'état manuel des imprimantes (`test_admin_update_printer_status`). + * La cohérence des transitions d'états de la file d'attente est gérée par `test_staff_change_status_auto` et `test_staff_must_set_price_for_awaiting_payment`, forçant l'attribution d'un prix avant le passage au paiement. + +* **F07-2 (Gestion du filament)** : + * L'administration des consommables est sécurisée par les tests `test_admin_create_filament` et `test_admin_can_update_quantity`, garantissant la mise à jour fiable des stocks de plastique en base de données. +--- + +## 5.2. Tests d'Interface (Playwright) et Validation Frontend + +Pour la partie émergée de l'iceberg (le Frontend), l'objectif initial intégrait le déploiement de tests de bout en bout (E2E) via l'outil **Playwright**. + +Cependant, comme évoqué dans notre bilan de projet (Section 6.1.3), le retard accumulé sur l'apprentissage d'Angular et la complexité de l'interfaçage asynchrone ont limité la couverture finale de ces tests E2E. + +## 5.2. Tests Manuels et Validation Fonctionnelle + +Pour pallier la couverture limitée des tests automatisés sur Playwright consécutive aux retards de développement, l'équipe a mis en place une **campagne rigoureuse de tests manuels**. Structurée autour d'un cahier de recette, cette approche a permis de confronter l'interface Angular aux règles métiers implémentées sur le backend Django. + +### 5.2.1. Scénarios validés et cahier de recette +Les tests ont été exécutés de bout en bout en simulant les deux rôles applicatifs clés de la plateforme : + +* **Parcours Étudiant (Interface Client)** : + * **F01 (Authentification)** : Validation de la connexion, de la déconnexion et de la persistance du jeton JWT. + * **F03-1 & F03-7 (Dépôt et Quantité)** : Téléversement réussi de fichiers `.stl` et spécification du nombre d'exemplaires. + * **F03-6 (Crédit restant)** : Vérification de l'affichage dynamique du solde de l'étudiant et du blocage de l'interface en cas de fonds insuffisants. + * **F06-3 (Matériaux)** : Sélection et transmission correcte du type de consommable (PLA / PETG). + +* **Parcours Bureau / Opérateur (Espace Administration)** : + * **F04-1 & F03-5 (Utilisateurs et Priorités)** : Modification des rôles (adhérents, bureau, projets) et vérification de l'impact visuel sur la hiérarchie des comptes. + * **F04-2 (Gestion des travaux)** : Assignation manuelle d'une imprimante à un travail de la file et mise à jour de son statut (`UP`, `USED`, `DOWN`). + * **F04-4 (Gestion crédit)** : Simulation de rechargements de comptes et de débits manuels. + * **F07-2 & F07-3 (Consommation)** : Décrémentation manuelle des stocks de filament en grammes et génération des rapports de consommation. + +### 5.2.2. Bilan de la validation manuelle +Bien que cette méthode soit plus chronophage qu'une suite de tests E2E automatisée, elle s'est avérée indispensable pour assurer la livraison. +\newpage + +# 6. Bilan du projet + +## 6.1. Plannings + +Le projet a accumulé d'importants retards, notamment en raison de la participation de certains membres de l'équipe à la Coupe de France de Robotique dans le cadre du projet GATE. Nous avons également sous-estimé la courbe d'apprentissage des technologies utilisées (Angular et Django). Le fait que tous les membres ne soient pas polyvalents sur le Frontend et le Backend a créé des complications de répartition. De plus, la communication avec les imprimantes (confiée à Arnaud) n'a pas pu être finalisée, ce qui a temporairement limité le périmètre d'automatisation du projet. + +### 6.1.1. Planning prévisionnel + +![Planning prévisionnel du projet PrIntech](planning-previsionnel.pdf) + +--- + +\newpage + +### 6.1.2. Planning final + +![Planning final du projet PrIntech](planning-final.pdf) + +--- + +\newpage + +### 6.1.3. Commentaires + +- **Choix du planning prévisionnel** : + - Angular étant appris en parallèle du projet, le développement Front n'a débuté qu'en février. + - Django étant mieux maîtrisé, son exécution a été plus rapide. La partie Moonraker/Klipper représentait la plus grande incertitude technique. + - La phase 3 a été la plus longue en raison de l'apprentissage en cours. + +- **Comparaison au planning réel** : + - Le projet a accumulé du retard en raison d'échéances académiques conflictuelles (projets GATE, partiels) et d'une sous-estimation de la complexité de certaines tâches. + - En réponse, nous avons réévalué le périmètre de notre projet afin de nous concentrer sur les fonctionnalités indispensables requises par le cahier des charges (les tests E2E ayant été mis de côté pour le moment). + - Ce retard n'est pas une fatalité : compte tenu de l'utilité concrète du projet, le développement sera poursuivi bien au-delà de la date de rendu académique. + +--- + +\newpage + +## 6.2. Plan de charges + +L'écart le plus marquant entre notre planification et la réalité réside dans la courbe d'apprentissage d'Angular et de Django, qui s'est révélée beaucoup plus longue et abrupte que prévu. N'ayant pas de compétences approfondies préalables sur ces frameworks, l'assimilation des concepts clés a nécessité un investissement temporel important. + +### 6.2.1. Plan de charges prévisionnel et final + +![Plan de Charge](planChargeSuiviActivites.png) + +--- + +\newpage + +### 6.2.3. Commentaires + +Nous avons optimisé le temps imparti. Celui-ci nous a permis de nous former non seulement sur les technologies auxquelles nous étions affectés, mais aussi de comprendre l'ensemble du code (Front et Back). + +Nous sommes ainsi parvenus à valoriser nos compétences grâce à une compréhension plus fine des abstractions techniques. Alors que Célian ne connaissait pas Django au démarrage du projet, qu'Adam débutait sur Angular et que Yanis était focalisé sur le Backend, nous sommes aujourd'hui tous en mesure de comprendre et de corriger le code de nos pairs à travers le système de Pull Requests sur Git. + +--- + +\newpage + +## 6.3. Difficultés rencontrées + +Malgré un produit final assez satisfaisant, le projet a été jalonné de difficultés qui nous ont poussés à adapter notre champ d'action : + +- La mise en commun du travail a été plus complexe que prévu, notamment lors de l'instauration des PR qui ont parfois perturbé le flux Git Flow classique. +- Les retards accumulés ont entamé l'efficacité globale de l'équipe malgré un démarrage rapide. +- **L'exploitation des API des imprimantes 3D** : Notre objectif initial était de relier directement les imprimantes à l'application via le logiciel FDM Monster. Cependant, cette configuration s'est révélée difficile en raison des systèmes propriétaires des machines. De plus, les machines étant hautement sollicitées sur le campus, nous avons temporairement configuré la plateforme pour qu'un administrateur valide et lance chaque impression manuellement. Nous continuons néanmoins à travailler sur l'automatisation complète de ce processus. + +### 6.3.1 Fonctionnalités réalisées ou non + +| ID | Fonctionnalité de l'application | Description technique | Priorité | Statut | +| :--- | :--- | :--- | :---: | :---: | +| **F01** | **Authentification** | Connexion et déconnexion. | Haute | **Réalisé** | +| **F02** | **Gestion des profils** | Affichage et modification des informations utilisateurs. | Basse | **Réalisé** | +| **F03** | **Gestion des impressions** | Flux complet d'impression 3D, du dépôt STL à la mise en file. | Haute | **Réalisé** | +| **F03-1** | **Déposer un STL** | Upload d'un fichier STL. | Haute | **Réalisé** | +| **F03-2** | **Slicer automatique** | Convertir le modèle en G-code via un slicer. | Moyenne | **Non réalisé** | +| **F03-3** | **Estimation du coût** | Calculer le prix d'impression à partir des paramètres. | Basse | **Non réalisé** | +| **F03-4** | **Position dans la file** | Afficher la place du travail dans la file. | Basse | **Non réalisé** | +| **F03-5** | **Priorité projets INTech** | Rôle dédié pour prioriser les impressions. | Basse | **Réalisé** | +| **F03-6** | **Crédit restant** | Afficher le crédit restant d'un utilisateur. | Basse | **Réalisé** | +| **F03-7** | **Quantité d'impressions** | Indiquer le nombre d'exemplaires à imprimer. | Basse | **Réalisé** | +| **F04** | **Espace administrateur** | Interface d'administration pour une gestion complète. | Moyenne | **Réalisé** | +| **F04-1** | **Gestion des utilisateurs** | Créer, modifier et supprimer les utilisateurs. | Haute | **Réalisé** | +| **F04-2** | **Gestion des travaux** | Créer, modifier et supprimer la file de travaux. | Moyenne | **Réalisé** | +| **F04-3** | **Reprise des erreurs** | Déclarer un travail en erreur et le remettre en file. | Moyenne | **Non réalisé** | +| **F04-4** | **Gestion crédit** | Gestion des crédits des utilisateurs (recharge). | Haute | **Réalisé** | +| **F05** | **Intégrations** | SSO et notifications externes. | Optionnelle | **Non réalisé** | +| **F05-1** | **Intégration CAS** | Inscription et connexion via le CAS de l'école. | Optionnelle | **Non réalisé** | +| **F05-2** | **Intégration Discord** | Utiliser des webhooks pour les notifications. | Optionnelle | **Non réalisé** | +| **F05-3** | **Intégration HelloAsso** | Créditer les comptes via HelloAsso. | Optionnelle | **Non réalisé** | +| **F06** | **Impression technique** | Options avancées d'impression. | Optionnelle | **Non réalisé** | +| **F06-1** | **Utilisation de supports** | Détecter la nécessité de supports. | Optionnelle | **Non réalisé** | +| **F06-2** | **Paramétrage du slicer** | Permettre l'upload d'un JSON de paramètres. | Optionnelle | **Non réalisé** | +| **F06-3** | **Choix des matériaux** | Choisir un plastique autre que le PLA. | Optionnelle | **Réalisé** | +| **F07** | **Gestion des filaments** | Administration des filaments et du stock. | Optionnelle | **Réalisé** | +| **F07-1** | **Activation des filaments** | Activer et désactiver des filaments. | Optionnelle | **Non réalisé** | +| **F07-2** | **Gestion du filament** | Gérer la quantité de filament restant. | Optionnelle | **Réalisé** | +| **F07-3** | **Gestion de consommation** | Rapport sur quantités, crédits et montants. | Optionnelle | **Réalisé** | + +--- + +\newpage + +# 7. Conclusion et perspectives + +Malgré les obstacles rencontrés, notre projet a abouti à une application web fonctionnelle, conforme aux critères essentiels fixés par notre cahier des charges. + +À long terme, nous souhaiterions peaufiner ce projet au-delà du cadre du livrable académique, que ce soit par la reprise du code par un autre groupe de projet informatique, ou directement par la maintenance applicative assurée par l'association INTech elle-même. La prochaine étape majeure consistera à déployer l'application en production, par exemple sur une machine virtuelle fournie par l'association MiNET, afin qu'elle devienne l'outil quotidien de l'association. + +Sur le plan personnel, ce projet s'est révélé extrêmement enrichissant pour de futurs ingénieurs. Il nous a permis de manipuler des technologies et des outils professionnels tout en répondant à un besoin concret du campus. Cette expérience précieuse pour des futurs projet surtout dans le web. Enfin, au-delà de la technique, cette expérience nous a confrontés aux réalités de la gestion de projet en équipe (gestion des retards, coordination du code), ce qui constituera un atout indéniable pour nos futurs projets professionnels. + +--- \ No newline at end of file diff --git a/Livrable/rapport-final.pdf b/Livrable/rapport-final.pdf new file mode 100644 index 0000000..e9c270d Binary files /dev/null and b/Livrable/rapport-final.pdf differ diff --git a/Livrable/rapport.pdf b/Livrable/rapport.pdf new file mode 100644 index 0000000..64228c7 Binary files /dev/null and b/Livrable/rapport.pdf differ diff --git a/Livrable/screenshots/admin-filaments.png b/Livrable/screenshots/admin-filaments.png new file mode 100644 index 0000000..1a1a1ff Binary files /dev/null and b/Livrable/screenshots/admin-filaments.png differ diff --git a/Livrable/screenshots/admin-operations.png b/Livrable/screenshots/admin-operations.png new file mode 100644 index 0000000..616d370 Binary files /dev/null and b/Livrable/screenshots/admin-operations.png differ diff --git a/Livrable/screenshots/admin-printers.png b/Livrable/screenshots/admin-printers.png new file mode 100644 index 0000000..2ec0109 Binary files /dev/null and b/Livrable/screenshots/admin-printers.png differ diff --git a/Livrable/screenshots/admin-requests.png b/Livrable/screenshots/admin-requests.png new file mode 100644 index 0000000..261e7a8 Binary files /dev/null and b/Livrable/screenshots/admin-requests.png differ diff --git a/Livrable/screenshots/admin-users.png b/Livrable/screenshots/admin-users.png new file mode 100644 index 0000000..8a25293 Binary files /dev/null and b/Livrable/screenshots/admin-users.png differ diff --git a/Livrable/screenshots/historique.png b/Livrable/screenshots/historique.png new file mode 100644 index 0000000..8737495 Binary files /dev/null and b/Livrable/screenshots/historique.png differ diff --git a/Livrable/screenshots/home.png b/Livrable/screenshots/home.png new file mode 100644 index 0000000..fa96edd Binary files /dev/null and b/Livrable/screenshots/home.png differ diff --git a/Livrable/screenshots/login.png b/Livrable/screenshots/login.png new file mode 100644 index 0000000..e86c100 Binary files /dev/null and b/Livrable/screenshots/login.png differ diff --git a/Livrable/screenshots/profile.png b/Livrable/screenshots/profile.png new file mode 100644 index 0000000..1dcbf92 Binary files /dev/null and b/Livrable/screenshots/profile.png differ diff --git a/Livrable/screenshots/request.png b/Livrable/screenshots/request.png new file mode 100644 index 0000000..f405412 Binary files /dev/null and b/Livrable/screenshots/request.png differ diff --git a/PrINTech-Back/.gitignore b/PrINTech-Back/.gitignore new file mode 100644 index 0000000..213e1d2 --- /dev/null +++ b/PrINTech-Back/.gitignore @@ -0,0 +1,5 @@ +back/media/ + +.venv/ +env/ +__pycache__/ \ No newline at end of file diff --git a/PrINTech-Back/back/__init__.py b/PrINTech-Back/back/__init__.py index e69de29..a663c73 100644 --- a/PrINTech-Back/back/__init__.py +++ b/PrINTech-Back/back/__init__.py @@ -0,0 +1 @@ +default_app_config = 'back.apps.api.ApiConfig' \ No newline at end of file diff --git a/PrINTech-Back/back/apps/api/admin.py b/PrINTech-Back/back/apps/api/admin.py index 8c38f3f..7ecee33 100644 --- a/PrINTech-Back/back/apps/api/admin.py +++ b/PrINTech-Back/back/apps/api/admin.py @@ -1,3 +1,5 @@ from django.contrib import admin - +from .models import Operation,User # Register your models here. +admin.site.register(User) +admin.site.register(Operation) \ No newline at end of file diff --git a/PrINTech-Back/back/apps/api/apps.py b/PrINTech-Back/back/apps/api/apps.py index a97d5a5..918e62a 100644 --- a/PrINTech-Back/back/apps/api/apps.py +++ b/PrINTech-Back/back/apps/api/apps.py @@ -1,6 +1,19 @@ from django.apps import AppConfig +from django.db.models.signals import post_migrate +def create_default_printers(sender, **kwargs): + from .models import Printer + + # Add all printers to the database if they don't exist + for printer_name in Printer.Name.values: + Printer.objects.get_or_create( + name=printer_name, + defaults={'status': Printer.Status.DOWN} + ) class ApiConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'back.apps.api' + + def ready(self): + post_migrate.connect(create_default_printers, sender=self) \ No newline at end of file diff --git a/PrINTech-Back/back/apps/api/migrations/0001_initial.py b/PrINTech-Back/back/apps/api/migrations/0001_initial.py index 7145403..c61fb9c 100644 --- a/PrINTech-Back/back/apps/api/migrations/0001_initial.py +++ b/PrINTech-Back/back/apps/api/migrations/0001_initial.py @@ -1,9 +1,11 @@ -# Generated by Django 6.0.2 on 2026-02-17 23:53 +# Generated by Django 6.0.2 on 2026-06-05 06:52 import django.contrib.auth.models import django.contrib.auth.validators +import django.core.validators import django.db.models.deletion import django.utils.timezone +import uuid from django.conf import settings from django.db import migrations, models @@ -13,85 +15,314 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), + ("auth", "0012_alter_user_first_name_max_length"), ] operations = [ migrations.CreateModel( - name='Filament', + name="Filament", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('colour', models.CharField(choices=[('RED', 'Red'), ('GREEN', 'Green'), ('YELLOW', 'Yellow'), ('BLUE', 'Blue'), ('MAGENTA', 'Magenta'), ('WHITE', 'White'), ('BLACK', 'Black'), ('PURPLE', 'Purple'), ('BROWN', 'Brown'), ('GREY', 'Grey'), ('ORANGE', 'Orange')], max_length=25)), - ('type', models.CharField(choices=[('PLA', 'Pla'), ('REINFORCED_PLA', 'Reinforced Pla'), ('PETG', 'Petg'), ('ABS', 'Abs'), ('TPE/TPU', 'Tpe')], max_length=25)), - ('quantity', models.PositiveIntegerField(default=0)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "color", + models.CharField( + default="#ffffff", + max_length=7, + validators=[ + django.core.validators.RegexValidator( + "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$" + ) + ], + ), + ), + ("color_name", models.TextField()), + ( + "type", + models.CharField( + choices=[("PLA", "Pla"), ("PETG", "Petg")], max_length=25 + ), + ), + ("quantity", models.PositiveIntegerField(default=0)), + ("price", models.PositiveIntegerField(default=0)), ], ), migrations.CreateModel( - name='Printer', + name="Printer", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('type', models.CharField(choices=[('SLA/DLP/MSLA', 'Sla'), ('SLS/MJF', 'Sls'), ('FDM/FFF', 'Fdm'), ('MJP', 'Mjp'), ('Binder_Jetting', 'Binder Jetting'), ('DMLS/SLM', 'Dmls')], max_length=25)), - ('status', models.CharField(choices=[('UP', 'Up'), ('DOWN', 'Down'), ('USED', 'Used')], max_length=25)), + ( + "name", + models.CharField( + choices=[ + ("CREALITY_K1C", "Creality K1C"), + ("SNAPMAKER_U1", "Snapmaker U1"), + ("PRUSA_MK3", "Prusa Mk3"), + ], + max_length=25, + primary_key=True, + serialize=False, + ), + ), + ( + "status", + models.CharField( + choices=[("UP", "Up"), ("DOWN", "Down"), ("USED", "Used")], + default="DOWN", + max_length=25, + ), + ), ], ), migrations.CreateModel( - name='User', + name="User", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('email', models.EmailField(max_length=254, unique=True)), - ('credit', models.DecimalField(decimal_places=2, default=0, max_digits=6)), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("email", models.EmailField(max_length=254, unique=True)), + ("credit", models.IntegerField(default=0)), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), ], options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'abstract': False, + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, }, managers=[ - ('objects', django.contrib.auth.models.UserManager()), + ("objects", django.contrib.auth.models.UserManager()), ], ), migrations.CreateModel( - name='File', + name="File", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('path', models.FileField(upload_to='uploads/%Y/%m/%d')), - ('number_of_printing', models.IntegerField(default=1)), - ('para_slicer', models.JSONField(blank=True, null=True)), - ('filament_id', models.ManyToManyField(to='api.filament')), - ('user_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("path", models.FileField(upload_to="uploads/%Y/%m/%d")), + ("number_of_printing", models.PositiveIntegerField(default=1)), + ("para_slicer", models.JSONField(blank=True, null=True)), + ( + "filament", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="api.filament", + ), + ), ], ), migrations.CreateModel( - name='Operation', + name="Request", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('datetime', models.DateTimeField(auto_now_add=True)), - ('operation_type', models.CharField(choices=[('WITHDRAW', 'Withdraw'), ('ADDING', 'Adding')], max_length=25)), - ('comment', models.TextField(blank=True, null=True)), - ('amount', models.PositiveIntegerField(default=0)), - ('signer_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='operation_signer', to=settings.AUTH_USER_MODEL)), - ('user_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='operation_receiver', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("price", models.PositiveIntegerField(default=0)), + ("comment", models.TextField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "status", + models.CharField( + choices=[ + ("SUBMITTED", "Submitted"), + ("AWAITING_PAYMENT", "Awaiting Payment"), + ("PENDING", "Pending"), + ("PRINTING", "Printing"), + ("AWAITING_PICKUP", "Awaiting Pickup"), + ("PICKED_UP", "Picked Up"), + ("FAILED", "Failed"), + ("CANCELED", "Canceled"), + ], + default="SUBMITTED", + max_length=25, + ), + ), + ( + "file", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="api.file", + ), + ), + ( + "printer", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="api.printer", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.CreateModel( - name='Request', + name="Operation", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('datetime', models.DateTimeField(auto_now_add=True)), - ('status', models.CharField(choices=[('PENDING', 'Pending'), ('PROCESSING', 'Processing'), ('COMPLETED', 'Completed'), ('FAILED', 'Failed')], max_length=25)), - ('file_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.file')), - ('printer_id', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.printer')), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "operation_type", + models.CharField( + choices=[ + ("CASH", "Cash"), + ("CARD", "Card"), + ("PAYMENT", "Payment"), + ("REFUND", "Refund"), + ], + max_length=25, + ), + ), + ("comment", models.TextField(blank=True, null=True)), + ("amount", models.IntegerField(default=0)), + ( + "agent", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="operation_agent", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "beneficiary", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="operation_beneficiary", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "request", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="api.request", + ), + ), ], ), ] diff --git a/PrINTech-Back/back/apps/api/migrations/0002_user_priority.py b/PrINTech-Back/back/apps/api/migrations/0002_user_priority.py new file mode 100644 index 0000000..60343fe --- /dev/null +++ b/PrINTech-Back/back/apps/api/migrations/0002_user_priority.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.2 on 2026-06-05 10:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="priority", + field=models.CharField( + choices=[ + ("ADHERENT", "Adherent"), + ("ROBOTECH", "Robotech"), + ("AUTOTECH", "Autotech"), + ("DRONE", "Drone"), + ("BUREAU", "Bureau"), + ], + default="ADHERENT", + max_length=25, + ), + ), + ] diff --git a/PrINTech-Back/back/apps/api/migrations/0003_rename_priority_user_role_user_profile_picture.py b/PrINTech-Back/back/apps/api/migrations/0003_rename_priority_user_role_user_profile_picture.py new file mode 100644 index 0000000..57a7b77 --- /dev/null +++ b/PrINTech-Back/back/apps/api/migrations/0003_rename_priority_user_role_user_profile_picture.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.2 on 2026-06-06 12:59 + +import back.apps.api.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0002_user_priority"), + ] + + operations = [ + migrations.RenameField( + model_name="user", + old_name="priority", + new_name="role", + ), + migrations.AddField( + model_name="user", + name="profile_picture", + field=models.ImageField( + blank=True, + null=True, + upload_to=back.apps.api.models.User.profile_pic_path, + ), + ), + ] diff --git a/PrINTech-Back/back/apps/api/models.py b/PrINTech-Back/back/apps/api/models.py index 944db9e..83162e4 100644 --- a/PrINTech-Back/back/apps/api/models.py +++ b/PrINTech-Back/back/apps/api/models.py @@ -2,45 +2,50 @@ from django.contrib.auth.models import AbstractUser from django.db import models +from django.core.validators import RegexValidator class User(AbstractUser): + + class Role(models.TextChoices): + ADHERENT = 'ADHERENT', 'Adherent' + ROBOTECH = 'ROBOTECH', 'Robotech' + AUTOTECH = 'AUTOTECH', 'Autotech' + DRONE = 'DRONE', 'Drone' + BUREAU = 'BUREAU', 'Bureau' + + def profile_pic_path(instance, filename): + ext = filename.split('.')[-1] + return f'profile_pics/{instance.id}.{ext}' + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) email = models.EmailField(blank=False, unique=True, null=False) credit = models.IntegerField(default=0) - + role = models.CharField(choices=Role.choices, max_length=25, null=False, blank=False, default=Role.ADHERENT) + profile_picture = models.ImageField(upload_to=profile_pic_path, null=True, blank=True) + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['username'] class Filament(models.Model): class Type(models.TextChoices): PLA = 'PLA' - REINFORCED_PLA = 'REINFORCED_PLA' PETG = 'PETG' - ABS = 'ABS' - TPE = 'TPE/TPU' - - class Color(models.TextChoices): - RED = 'RED' - GREEN = 'GREEN' - YELLOW = 'YELLOW' - BLUE = 'BLUE' - MAGENTA = 'MAGENTA' - WHITE = 'WHITE' - BLACK = 'BLACK' - PURPLE = 'PURPLE' - BROWN = 'BROWN' - GREY = 'GREY' - ORANGE = 'ORANGE' - - colour = models.CharField(choices=Color.choices, max_length=25, null=False, blank=False) + + color = models.CharField( + max_length=7, + default='#ffffff', + validators=[RegexValidator(r'^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$')] + ) + color_name = models.TextField(null=False, blank=False) type = models.CharField(choices=Type.choices, max_length=25, null=False, blank=False) quantity = models.PositiveIntegerField(default=0) + price = models.PositiveIntegerField(default=0) class File(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - user_id=models.ForeignKey(User, on_delete=models.CASCADE) - filament_id=models.ManyToManyField(Filament) + filament=models.ForeignKey(Filament, on_delete=models.CASCADE, null=True) path = models.FileField(upload_to='uploads/%Y/%m/%d') number_of_printing = models.PositiveIntegerField(default=1) para_slicer = models.JSONField(null=True, blank=True) @@ -53,44 +58,55 @@ class Status(models.TextChoices): DOWN = 'DOWN' USED = 'USED' - class Type(models.TextChoices): - SLA = 'SLA/DLP/MSLA' #Resin - SLS = 'SLS/MJF' #Powder - FDM = 'FDM/FFF' #Filament - MJP = 'MJP' #MultiJet Printing - Binder_Jetting = 'Binder_Jetting' - DMLS = 'DMLS/SLM' #Metal - - type = models.CharField(choices=Type.choices, max_length=25, null=False, blank=False) + class Name(models.TextChoices): + CREALITY_K1C = 'CREALITY_K1C' + SNAPMAKER_U1 = 'SNAPMAKER_U1' + PRUSA_MK3 = 'PRUSA_MK3' + + name = models.CharField(primary_key=True,choices=Name.choices, max_length=25, null=False, blank=False) status = models.CharField(choices=Status.choices, max_length=25, null=False, blank=False, default=Status.DOWN) + class Request(models.Model): class Status(models.TextChoices): + SUBMITTED = 'SUBMITTED' + AWAITING_PAYMENT = 'AWAITING_PAYMENT' PENDING = 'PENDING' - PROCESSING = 'PROCESSING' - COMPLETED = 'COMPLETED' + PRINTING = 'PRINTING' + AWAITING_PICKUP = 'AWAITING_PICKUP' + PICKED_UP = 'PICKED_UP' FAILED = 'FAILED' + CANCELED = 'CANCELED' id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - file_id = models.ForeignKey(File, on_delete=models.CASCADE) - printer_id = models.ForeignKey(Printer, on_delete=models.CASCADE, null=True) + user=models.ForeignKey(User, on_delete=models.CASCADE) + file = models.ForeignKey(File, on_delete=models.CASCADE, null=True) + printer = models.ForeignKey(Printer, on_delete=models.CASCADE, null=True) + price = models.PositiveIntegerField(default=0) + comment = models.TextField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) - status = models.CharField(choices=Status.choices, max_length=25, null=False, blank=False, default=Status.PENDING) + status = models.CharField(choices=Status.choices, max_length=25, null=False, blank=False, default=Status.SUBMITTED) + @property + def is_paid(self): + return hasattr(self, 'operation') class Operation(models.Model): class Type(models.TextChoices): CASH = 'CASH' CARD = 'CARD' + PAYMENT = 'PAYMENT' + REFUND = 'REFUND' + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - beneficiary_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='operation_beneficiary') - agent_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='operation_agent') + beneficiary = models.ForeignKey(User, on_delete=models.CASCADE, related_name='operation_beneficiary') + agent = models.ForeignKey(User, on_delete=models.CASCADE, related_name='operation_agent') created_at = models.DateTimeField(auto_now_add=True) operation_type = models.CharField(choices=Type.choices, max_length=25, null=False, blank=False) comment = models.TextField(null=True, blank=True) amount = models.IntegerField(default=0) - + request = models.ForeignKey(Request, on_delete=models.CASCADE, null=True) diff --git a/PrINTech-Back/back/apps/api/serializers.py b/PrINTech-Back/back/apps/api/serializers.py index 51786a6..366f5a8 100644 --- a/PrINTech-Back/back/apps/api/serializers.py +++ b/PrINTech-Back/back/apps/api/serializers.py @@ -1,6 +1,8 @@ from django.contrib.auth.password_validation import validate_password from rest_framework import serializers -from .models import User +from .models import User, Operation, Request, File, Filament, Printer +from django.db import transaction + class ChangePasswordSerializer(serializers.Serializer): @@ -46,6 +48,98 @@ def validate_password(self, password): class Meta: model = User - fields = ["id", "username", "password", "email"] - extra_kwargs = {"password": {"write_only": True}} + fields = ["id", "username", "password", "email", "credit", "is_staff", "profile_picture","role"] + extra_kwargs = { + "password": {"write_only": True}, + "credit": {"read_only": True}, + "is_staff": {"read_only": True}, + "role": {"read_only": True} + } + + +class AdminUserSerializer(serializers.ModelSerializer): + password = serializers.CharField(write_only=True, required=False) + + class Meta: + model = User + fields = ["id", "username", "email", "password", "credit", "is_staff", "is_active", "profile_picture", "role"] + read_only_fields = ["id"] + + def validate_password(self, password): + validate_password(password) + return password + + def create(self, validated_data): + password = validated_data.pop("password") + user = User.objects.create_user(password=password, **validated_data) + return user + + def update(self, instance, validated_data): + password = validated_data.pop("password", None) + for key, value in validated_data.items(): + setattr(instance, key, value) + if password: + instance.set_password(password) + instance.save() + return instance + +class OperationSerializer(serializers.ModelSerializer): + class Meta: + model = Operation + fields = ['id', 'beneficiary', 'agent', 'amount', 'operation_type', 'comment', 'created_at','request'] + read_only_fields = ['agent', 'id', 'created_at'] + + def create(self, validated_data): + with transaction.atomic(): + beneficiary = validated_data['beneficiary'] + amount = validated_data['amount'] + if amount<0 and beneficiary.credit<-amount: + raise serializers.ValidationError("Insufficient funds") + beneficiary.credit += amount + beneficiary.save() + return super().create(validated_data) + + +class FileSerializer(serializers.ModelSerializer): + class Meta: + model = File + fields = ["path", "number_of_printing", "filament", "para_slicer"] + read_only_fields = ['id', 'user','status', 'printer', 'created_at', 'status'] + +class RequestSerializer(serializers.ModelSerializer): + file = FileSerializer(many=False, read_only=True) + path = serializers.FileField(write_only=True) + number_of_printing = serializers.IntegerField(write_only=True, default=1) + para_slicer = serializers.JSONField(write_only=True, required=False) + filament = serializers.PrimaryKeyRelatedField( + write_only=True, queryset=Filament.objects.all(), required=True + ) + class Meta: + model = Request + fields = ['id', 'user', 'file', 'printer', 'price', 'filament', 'comment', 'created_at','status',"path","number_of_printing", "para_slicer"] + read_only_fields = ['id', 'user','file', 'printer', 'price', 'created_at', 'status'] + + def create(self, validated_data): + path = validated_data.pop('path') + number_of_printing = validated_data.pop('number_of_printing') + para_slicer = validated_data.pop('para_slicer', {}) + filament = validated_data.pop('filament', None) + + new_file = File.objects.create( + path=path, + number_of_printing=number_of_printing, + para_slicer=para_slicer, + filament=filament + ) + return Request.objects.create(file=new_file, **validated_data) + +class FilamentSerializer(serializers.ModelSerializer): + class Meta: + model = Filament + fields = ['id', 'color', 'color_name', 'type', 'quantity'] + +class PrinterSerializer(serializers.ModelSerializer): + class Meta: + model = Printer + fields = ['name', 'status'] \ No newline at end of file diff --git a/PrINTech-Back/back/apps/api/tests.py b/PrINTech-Back/back/apps/api/tests.py index 7ce503c..01337f0 100644 --- a/PrINTech-Back/back/apps/api/tests.py +++ b/PrINTech-Back/back/apps/api/tests.py @@ -1,3 +1,797 @@ -from django.test import TestCase +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APITestCase +from rest_framework_simplejwt.tokens import RefreshToken +from django.core.files.uploadedfile import SimpleUploadedFile +from django.contrib.auth import get_user_model +from PIL import Image +import io -# Create your tests here. +User = get_user_model() +import json +from .models import Request, File, Operation, Filament, Printer + +class UserMeTests(APITestCase): + + def setUp(self): + self.initial_balance = 100 + self.user = User.objects.create_user( + username='scammerrr', + password='password123', + email='scammer67@intech.com', + credit=self.initial_balance + ) + refresh = RefreshToken.for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + self.me_url = reverse('user_info') + + def test_user_cannot_update_credit_manually(self): + response = self.client.patch(self.me_url, {'credit': 9999}) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.user.refresh_from_db() + self.assertEqual(self.user.credit, self.initial_balance) + + def test_delete_profile_picture_when_none_exists(self): + self.user.profile_picture = None + self.user.save() + + response = self.client.delete(self.me_url) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data['error'], "Aucune photo de profil à supprimer.") + + +class UserChangePasswordDetailedTests(APITestCase): + + def setUp(self): + self.user = User.objects.create_user(username='Billy', password='VeryySecure2!', email='mod@intech.com') + refresh = RefreshToken.for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + self.password_url = reverse('change-password') + + def test_change_password_mismatched_confirmation(self): + data = { + "old_password": "VeryySecure2!", + "new_password": "Passworddd7676767!", + "confirm_password": "DifferentPassworddd7676767!" + } + response = self.client.put(self.password_url, data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("non_field_errors", response.data) + + def test_change_password_wrong_old_password(self): + data = { + "old_password": "WrongPassword1!", + "new_password": "BrandNewPassword67!", + "confirm_password": "BrandNewPassword67!" + } + response = self.client.put(self.password_url, data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("old_password", response.data) + +class AdminUserViewSetTests(APITestCase): + + def setUp(self): + self.admin = User.objects.create_user( + username='WilliamPrez', + password='Adminn123!', + email='william@intech.com', + is_staff=True + ) + self.regular_user = User.objects.create_user( + username='subordinate', + password='UserPassword123!', + email='zzz@intech.com', + credit=10 + ) + + refresh = RefreshToken.for_user(self.admin) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + + self.list_url = reverse('admin-user-list') + self.detail_url = reverse('admin-user-detail', args=[self.regular_user.id]) + + def test_admin_can_list_all_users_sorted_by_email(self): + """Admin should get a list of all users, ordered by email address.""" + response = self.client.get(self.list_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 2) + self.assertEqual(response.data[0]['email'], self.admin.email) # car w > z + + def test_admin_can_manually_adjust_user_credit(self): + """Admin uses AdminUserSerializer which allows direct credit modification.""" + data = {"credit": 500} + response = self.client.patch(self.detail_url, data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.regular_user.refresh_from_db() + self.assertEqual(self.regular_user.credit, 500) + + def test_admin_can_deactivate_user(self): + """Admin can change account status flags like is_active.""" + data = {"is_active": False} + response = self.client.patch(self.detail_url, data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.regular_user.refresh_from_db() + self.assertFalse(self.regular_user.is_active) + + def test_admin_can_create_user_with_explicit_password(self): + """Admin can explicitly create a new valid user profile.""" + data = { + "username": "created_by_admin", + "email": "created@intech.com", + "password": "ValidPassword123!", + "credit": 6767 + } + response = self.client.post(self.list_url, data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + new_user = User.objects.get(email="created@intech.com") + self.assertTrue(new_user.check_password("ValidPassword123!")) + + def test_regular_user_cannot_access_admin_user_endpoints(self): + """Non-staff users must be rejected immediately.""" + refresh = RefreshToken.for_user(self.regular_user) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + + response = self.client.get(self.list_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + +class UserProfilePictureTests(APITestCase): + + def setUp(self): + self.user = User.objects.create_user(username='CelianTest', password='password123', email='celiantest@intech.com') + refresh = RefreshToken.for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + self.me_url = reverse('user_info') + + img = Image.new('RGB', (100, 100), color='red') + img_bytes = io.BytesIO() + img.save(img_bytes, format='JPEG') + img_bytes.seek(0) # Avoid blank file read + self.test_image = SimpleUploadedFile( + name='pp.jpg', + content=img_bytes.read(), + content_type='image/jpeg' + ) + + def test_upload_profile_picture(self): + response = self.client.patch(self.me_url, {'profile_picture': self.test_image}, format='multipart') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.user.refresh_from_db() + self.assertTrue(self.user.profile_picture) + + def test_modify_profile_picture(self): + self.client.patch(self.me_url, {'profile_picture': self.test_image}, format='multipart') + self.user.refresh_from_db() + old_name = self.user.profile_picture.name + + new_img = Image.new('RGB', (100, 100), color='blue') + new_img_bytes = io.BytesIO() + new_img.save(new_img_bytes, format='JPEG') + new_image_file = SimpleUploadedFile(name='new_avatar.jpg', content=new_img_bytes.getvalue(), content_type='image/jpeg') + + response = self.client.patch(self.me_url, {'profile_picture': new_image_file}, format='multipart') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.user.refresh_from_db() + + self.assertNotEqual(self.user.profile_picture.name, old_name) + + def test_delete_profile_picture(self): + self.client.patch(self.me_url, {'profile_picture': self.test_image}, format='multipart') + + response = self.client.delete(self.me_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.user.refresh_from_db() + self.assertFalse(self.user.profile_picture) + self.assertTrue(User.objects.filter(id=self.user.id).exists()) + + def test_unauthenticated_cannot_upload(self): + self.client.credentials() + response = self.client.patch(self.me_url, {'profile_picture': self.test_image}, format='multipart') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + +class UserRoleAndPriorityTests(APITestCase): + + def setUp(self): + # Create an Admin to access admin endpoints + self.admin = User.objects.create_user( + username='admin_boss', + password='Password123!', + email='admin@intech.com', + is_staff=True + ) + + # Create users with different roles + self.user_bureau = User.objects.create_user( + username='bureau_member', password='Password123!', email='bureau@intech.com', role='BUREAU' + ) + self.user_robotech = User.objects.create_user( + username='robotech_member', password='Password123!', email='robotech@intech.com', role='ROBOTECH' + ) + self.user_adherent = User.objects.create_user( + username='adherent_member', password='Password123!', email='adherent@intech.com', role='ADHERENT' + ) + + # Create a shared dummy filament and file setup for requests + self.filament = Filament.objects.create( + color='#FFFFFF', color_name='Blanc', type='PLA', quantity=10 + ) + test_file = SimpleUploadedFile(name='role_test.stl', content=b'data', content_type='model/stl') + self.file_obj = File.objects.create(path=test_file, number_of_printing=1, filament=self.filament) + + # Authenticate the client as admin for priority testing + refresh = RefreshToken.for_user(self.admin) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + self.admin_request_list_url = reverse('admin-request-list') + + def test_admin_request_list_priority_sorting(self): + """ + Verify that the admin viewset orders requests primarily by priority rank: + BUREAU (0) > ROBOTECH/AUTOTECH/DRONE (1) > ADHERENT (2) + """ + # Create requests in mixed/scrambled order of roles + req_adherent = Request.objects.create(user=self.user_adherent, status='SUBMITTED', comment="Adherent Req", file=self.file_obj) + req_bureau = Request.objects.create(user=self.user_bureau, status='SUBMITTED', comment="Bureau Req", file=self.file_obj) + req_robotech = Request.objects.create(user=self.user_robotech, status='SUBMITTED', comment="Robotech Req", file=self.file_obj) + + response = self.client.get(self.admin_request_list_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Extract the comments in order from the response payload + returned_comments = [item['comment'] for item in response.data] + + # Assert specific priority rank hierarchy sorting + expected_order = ["Bureau Req", "Robotech Req", "Adherent Req"] + self.assertEqual(returned_comments, expected_order) + + def test_default_role_is_adherent(self): + """Ensure a newly registered user defaults safely to the ADHERENT role.""" + new_user = User.objects.create_user( + username='fresh_user', + password='Password123!', + email='fresh@intech.com' + ) + self.assertEqual(new_user.role, User.Role.ADHERENT) + + +class UserRoleTests(APITestCase): + + def setUp(self): + self.user = User.objects.create_user( + username='cel', + password='Password123!', + email='cel@intech.com', + role='ADHERENT' + ) + self.admin = User.objects.create_user( + username='sek', + password='Password123!', + email='sek@intech.com', + is_staff=True + ) + + self.me_url = reverse('user_info') + self.admin_user_detail_url = reverse('admin-user-detail', args=[self.user.id]) + + def test_regular_user_cannot_change_own_role(self): + refresh = RefreshToken.for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + + response = self.client.patch(self.me_url, {'role': 'BUREAU'}) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.user.refresh_from_db() + self.assertEqual(self.user.role, 'ADHERENT') + + def test_admin_can_modify_user_role(self): + refresh = RefreshToken.for_user(self.admin) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + + response = self.client.patch(self.admin_user_detail_url, {'role': 'ROBOTECH'}) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.user.refresh_from_db() + self.assertEqual(self.user.role, 'ROBOTECH') + +class RequestViewSetTests(APITestCase): + def setUp(self): + self.initial_balance = 100 + + self.user = User.objects.create_user(username='Celian', password='password123', email='celian@intech.com', credit=self.initial_balance) + self.user2 = User.objects.create_user(username='Yanis', password='password123', email='yanis@intech.com', credit=self.initial_balance) + + refresh = RefreshToken.for_user(self.user) + self.access_token = str(refresh.access_token) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {self.access_token}') + + self.filament = Filament.objects.create( + color='#FF0000', + color_name='Rouge', + type='PLA', + quantity=5 + ) + + test_file = SimpleUploadedFile( + name='test.stl', + content=b'blabla', + content_type='model/stl' + ) + + file = File.objects.create( + path=test_file, + number_of_printing=1, + filament=self.filament + ) + + self.req_usr1 = Request.objects.create( + user=self.user, status='AWAITING_PAYMENT', price=10, comment="Robotech", file=file + ) + self.req_usr2 = Request.objects.create( + user=self.user2, status='SUBMITTED', comment="Autotech", file=file + ) + + self.list_url = reverse('request-list') + self.pay_url = reverse('request-pay', args=[self.req_usr1.id]) + self.cancel_url = reverse('request-cancel', args=[self.req_usr1.id]) + + def test_list_requests_authenticated(self): + response = self.client.get(self.list_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['comment'], "Robotech") + + def test_no_access_other_user_request(self): + url = reverse('request-detail', args=[self.req_usr2.id]) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_cannot_change_status(self): + data = {'status': 'PRINTING'} + response = self.client.patch(self.list_url, data) + self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + + + def test_create_request(self): + + test_file = SimpleUploadedFile( + name='test.stl', + content=b'blabla', + content_type='model/stl' + ) + + data = { + "comment": "test", + "path": test_file, + "number_of_printing": 1, + "para_slicer": json.dumps({"layer_height": 0.2, "infill": "20%"}), + "filament": self.filament.id + } + response = self.client.post(self.list_url, data, format='multipart') + #response = self.client.post(self.list_url, data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Request.objects.count(), 3) + + def test_missing_token(self): + self.client.credentials() # Clear the header + response = self.client.get(self.list_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_pay_request_success(self): + response = self.client.post(self.pay_url) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.req_usr1.refresh_from_db() + self.user.refresh_from_db() + + self.assertEqual(self.req_usr1.status, 'PENDING') + expected_balance = self.initial_balance - self.req_usr1.price + self.assertEqual(self.user.credit, expected_balance) + self.assertEqual(self.req_usr1.operation_set.filter(operation_type='PAYMENT').count(), 1) + + def test_cancel_request_with_refund(self): + self.client.post(self.pay_url) + response = self.client.post(self.cancel_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.user.refresh_from_db() + self.assertEqual(self.user.credit, self.initial_balance) + self.req_usr1.refresh_from_db() + self.assertEqual(self.req_usr1.status, 'CANCELED') + self.assertEqual(self.req_usr1.operation_set.count(), 2) + + def test_cancel_request_multiple_payments_conflict(self): + self.req_usr1.status = 'PENDING' + self.req_usr1.save() + + for _ in range(2): + Operation.objects.create( + beneficiary=self.user, agent=self.user, amount=-10, + operation_type='PAYMENT', request=self.req_usr1 + ) + + response = self.client.post(self.cancel_url) + self.assertEqual(response.status_code, status.HTTP_409_CONFLICT) + + def test_pay_request_insufficient_funds(self): + USER_BALANCE = -1 #always insufficient + self.user.credit = USER_BALANCE + self.user.save() + + response = self.client.post(self.pay_url) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.req_usr1.refresh_from_db() + self.assertEqual(self.req_usr1.status, 'AWAITING_PAYMENT') + self.user.refresh_from_db() + self.assertEqual(self.user.credit, USER_BALANCE) + + def test_pay_request_requires_price(self): + self.req_usr1.price = 0 + self.req_usr1.save() + + response = self.client.post(self.pay_url) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data['error'], 'Request price is not set.') + + def test_cannot_pay_other_user_request(self): + usr2_pay_url = reverse('request-pay', args=[self.req_usr2.id]) + response = self.client.post(usr2_pay_url) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(Operation.objects.filter(request=self.req_usr2).count(), 0) + + def test_cannot_cancel_other_user_request(self): + usr2_cancel_url = reverse('request-cancel', args=[self.req_usr2.id]) + response = self.client.post(usr2_cancel_url) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.req_usr2.refresh_from_db() + self.assertEqual(self.req_usr2.status, 'SUBMITTED') + +class AdminRequestViewTests(APITestCase): + def setUp(self): + self.admin = User.objects.create_user( + username='william', + password='password123', + email='william@intech.com', + is_staff=True + ) + + refresh = RefreshToken.for_user(self.admin) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + + self.filament = Filament.objects.create( + color='#FF0000', + color_name='Rouge', + type='PLA', + quantity=5 + ) + test_file = SimpleUploadedFile(name='test.stl', content=b'data', content_type='model/stl') + self.file_obj = File.objects.create(path=test_file, number_of_printing=1, filament=self.filament) + + self.print_req = Request.objects.create( + user=self.admin, + status='SUBMITTED', + comment="Admin Test", + file=self.file_obj + ) + + self.change_status_url = reverse('admin-request-change-status', args=[self.print_req.id]) + + def test_staff_change_status_manually(self): + data = {'status': 'PRINTING'} + response = self.client.patch(self.change_status_url, data) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data['error'], 'New requests must move to awaiting payment first.') + self.print_req.refresh_from_db() + self.assertEqual(self.print_req.status, 'SUBMITTED') + + def test_staff_change_status_auto(self): + expected_status = Request.Status.AWAITING_PAYMENT + + response = self.client.patch(self.change_status_url, {'price': 25}) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.print_req.refresh_from_db() + self.assertEqual(self.print_req.status, expected_status) + self.assertEqual(self.print_req.price, 25) + + def test_staff_cannot_auto_skip_payment(self): + self.print_req.status = Request.Status.AWAITING_PAYMENT + self.print_req.save() + + response = self.client.patch(self.change_status_url, {}) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data['error'], 'Request is waiting for user payment.') + + def test_staff_must_set_price_for_awaiting_payment(self): + response = self.client.patch(self.change_status_url, {'status': 'AWAITING_PAYMENT'}) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data['error'], 'A price is required before setting awaiting payment.') + + def test_staff_change_status_auto_limit(self): + statuses = Request.Status.values + self.print_req.status = statuses[-2] + self.print_req.save() + + response = self.client.patch(self.change_status_url, {}) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data['error'], 'Already Completed.') + + def test_non_staff_access(self): + user = User.objects.create_user(username='Ilo', password='password123') + refresh = RefreshToken.for_user(user) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + + response = self.client.patch(self.change_status_url, {'status': 'PRINTING'}) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +class UserOperationTests(APITestCase): + def setUp(self): + self.initial_balance = 100 + self.user1 = User.objects.create_user( + username='Celian', + password='password123', + email='celian@intech.com', + credit=self.initial_balance + ) + self.user2 = User.objects.create_user( + username='Yanis', + password='password123', + email='yanis@intech.com', + credit=self.initial_balance + ) + + self.admin = User.objects.create_user( + username='William', + password='password123', + email='william@intech.com', + is_staff=True + ) + + refresh = RefreshToken.for_user(self.user1) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {str(refresh.access_token)}') + + Operation.objects.create( + beneficiary=self.user1, + agent=self.admin, + amount=20, + operation_type='CASH' + ) + + + self.url = reverse('operation-list') + + def test_user_list_own_operations_only(self): + Operation.objects.create( + beneficiary=self.user2, + agent=self.admin, + amount=50, + operation_type='CASH' + ) + + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['amount'], 20) + + def test_user_cannot_create_operation(self): + data = {"beneficiary": self.user1, "amount": 100, "operation_type": "CASH"} + response = self.client.post(self.url, data) + self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + + +class AdminOperationTests(APITestCase): + def setUp(self): + self.initial_balance = 100 + self.admin = User.objects.create_user( + username='Celian', + password='password123', + email='celian@intech.com', + is_staff=True + ) + self.beneficiary = User.objects.create_user( + username='Ilo', + password='password123', + email='ilo@test.com', + credit=self.initial_balance + ) + + refresh = RefreshToken.for_user(self.admin) + self.access_token = str(refresh.access_token) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {self.access_token}') + + self.url = reverse('admin-operation-list') + + def test_admin_deposit_cash(self): + change = 50 + data = { + "beneficiary": self.beneficiary.id, + "amount": change, + "operation_type": "CASH" + } + response = self.client.post(self.url, data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + self.beneficiary.refresh_from_db() + self.assertEqual(self.beneficiary.credit, self.initial_balance + change) + + def test_admin_insufficient_funds(self): + data = { + "beneficiary": self.beneficiary.id, + "amount": -150, + "operation_type": "PAYMENT" + } + response = self.client.post(self.url, data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_admin_list(self): + Operation.objects.create( + beneficiary=self.beneficiary, + agent=self.admin, + amount=10, + operation_type='CASH' + ) + + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(len(response.data) >= 1) + + +class FilamentTests(APITestCase): + def setUp(self): + self.initial_balance=100 + self.user = User.objects.create_user( + username='Celian', + password='password123', + email='celian@intech.com', + credit=self.initial_balance + ) + + self.admin = User.objects.create_user( + username='William', + password='password123', + email='william@intech.com', + is_staff=True + ) + + # Create initial data + self.filament = Filament.objects.create( + color='#FF0000', + color_name='Rouge', + type='PLA', + quantity=5 + ) + + self.public_list_url = reverse('filament-list') + self.admin_list_url = reverse('filament-admin-list') + self.admin_detail_url = reverse('filament-admin-detail', args=[self.filament.id]) + + + def test_user_can_list_filaments(self): + refresh = RefreshToken.for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + + response = self.client.get(self.public_list_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['color_name'], 'Rouge') + + def test_user_cannot_create_filament(self): + """ReadOnlyModelViewSet should block POST requests from regular users.""" + refresh = RefreshToken.for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + + data = {"color": "#00FF00", "color_name": "Green", "type": "PETG", "quantity": 10} + response = self.client.post(self.public_list_url, data) + self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + + def test_user_cannot_access_admin_endpoint(self): + refresh = RefreshToken.for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + + response = self.client.get(self.admin_list_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + + def test_admin_create_filament(self): + refresh = RefreshToken.for_user(self.admin) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + + data = { + "color": "#0000FF", + "color_name": "Bleu", + "type": "PLA", + "quantity": 5 + } + response = self.client.post(self.admin_list_url, data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Filament.objects.count(), 2) + + def test_admin_can_update_quantity(self): + refresh = RefreshToken.for_user(self.admin) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + + data = {"quantity": 67} + response = self.client.patch(self.admin_detail_url, data) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.filament.refresh_from_db() + self.assertEqual(self.filament.quantity, 67) + + def test_invalid_hex_code(self): + refresh = RefreshToken.for_user(self.admin) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + + data = { + "color": "NOT A HEX CODE", + "color_name": "blabla", + "type": "PLA", + "quantity": 1 + } + response = self.client.post(self.admin_list_url, data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_admin_delete_filament(self): + refresh = RefreshToken.for_user(self.admin) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + + response = self.client.delete(self.admin_detail_url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Filament.objects.count(), 0) + +class PrinterViewTests(APITestCase): + def setUp(self): + self.user = User.objects.create_user(username='Celian', password='password123', email='celian@intech.com') + self.admin = User.objects.create_user(username='William', password='password123', email='william@intech.com', is_staff=True) + + for printer in Printer.Name.values: + Printer.objects.get_or_create(name=printer) + + self.public_list_url = reverse('printer-list') + self.admin_list_url = reverse('admin-printer-list') + self.printer = Printer.objects.first() + self.admin_detail_url = reverse('admin-printer-detail', args=[self.printer.name]) + + def test_list_printers(self): + refresh = RefreshToken.for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + response = self.client.get(self.public_list_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 3) + + def test_readonly(self): + data = {'status': 'UP'} + response = self.client.patch(self.public_list_url, data) + self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + + def test_admin_update_printer_status(self): + refresh = RefreshToken.for_user(self.admin) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + data = {'status': 'UP'} + response = self.client.patch(self.admin_detail_url, data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.printer.refresh_from_db() + self.assertEqual(self.printer.status, 'UP') + + def test_admin_access(self): + refresh = RefreshToken.for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + data = {'status': 'UP'} + response = self.client.patch(self.admin_detail_url, data) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_admin_cannot_create_printer(self): + refresh = RefreshToken.for_user(self.admin) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + data = {'name': 'CREALITY_K1C', 'status': 'UP'} + response = self.client.post(self.admin_list_url, data) + self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + + def test_admin_cannot_delete_printer(self): + refresh = RefreshToken.for_user(self.admin) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') + response = self.client.delete(self.admin_detail_url) + self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) \ No newline at end of file diff --git a/PrINTech-Back/back/apps/api/views.py b/PrINTech-Back/back/apps/api/views.py index d7fa2fc..028452e 100644 --- a/PrINTech-Back/back/apps/api/views.py +++ b/PrINTech-Back/back/apps/api/views.py @@ -1,11 +1,15 @@ from rest_framework.response import Response from rest_framework_simplejwt.tokens import RefreshToken, AccessToken -from .models import User -from rest_framework import permissions, viewsets, generics, status +from .models import User, Operation, Request, Filament, Printer +from django.db.models import Case, When, IntegerField, Value +from rest_framework import permissions, viewsets, mixins, generics, status, serializers +from rest_framework.decorators import action from django.shortcuts import render -from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.permissions import AllowAny, IsAuthenticated,IsAdminUser from rest_framework.throttling import UserRateThrottle -from .serializers import UserSerializer, ChangePasswordSerializer +from .serializers import UserSerializer, ChangePasswordSerializer, OperationSerializer, RequestSerializer,FilamentSerializer, PrinterSerializer +from .serializers import AdminUserSerializer +from django.db import transaction class CreateUserView(generics.CreateAPIView): queryset = User.objects.all() @@ -34,7 +38,7 @@ def create(self, request, *args, **kwargs): ) -class UserMeView(generics.RetrieveUpdateAPIView): +class UserMeView(generics.RetrieveUpdateDestroyAPIView): permission_classes = [IsAuthenticated] serializer_class = UserSerializer throttle_classes = [UserRateThrottle] @@ -42,6 +46,16 @@ class UserMeView(generics.RetrieveUpdateAPIView): def get_object(self): return self.request.user + def destroy(self, request, *args, **kwargs): + """Deletes ONLY profile picture, not the user account.""" + user = self.get_object() + if user.profile_picture: + user.profile_picture.delete(save=False) + user.profile_picture = None + user.save() + return Response({"message": "Photo de profil supprimée avec succès."}, status=status.HTTP_200_OK) + return Response({"error": "Aucune photo de profil à supprimer."}, status=status.HTTP_400_BAD_REQUEST) + class ChangePasswordView(generics.UpdateAPIView): permission_classes = [IsAuthenticated] serializer_class = ChangePasswordSerializer @@ -56,3 +70,287 @@ def update(self, request, *args, **kwargs): {"message": "Mot de passe changé avec succès."}, status=status.HTTP_200_OK ) +class OperationView(mixins.ListModelMixin, + mixins.RetrieveModelMixin, + viewsets.GenericViewSet): + serializer_class = OperationSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + return Operation.objects.filter(beneficiary=self.request.user) + +class AdminOperationView(mixins.CreateModelMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + viewsets.GenericViewSet): + serializer_class = OperationSerializer + permission_classes = [permissions.IsAdminUser] + queryset = Operation.objects.all() + + def perform_create(self, serializer): + serializer.save(agent=self.request.user) + +class RequestView(mixins.CreateModelMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + viewsets.GenericViewSet): + serializer_class = RequestSerializer + permission_classes=[IsAuthenticated] + def get_queryset(self): + return Request.objects.filter(user=self.request.user) + + def perform_create(self, serializer): + serializer.save(user=self.request.user, status='SUBMITTED') + + @transaction.atomic + @action(detail=True, methods=['post']) + def relaunch(self, request, pk=None): + previous_request = self.get_object() + + if previous_request.file is None: + return Response( + {"error": "Cannot relaunch a request without file."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + relaunched_request = Request.objects.create( + user=request.user, + file=previous_request.file, + printer=None, + comment=previous_request.comment, + status='SUBMITTED', + ) + + serializer = self.get_serializer(relaunched_request) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + @transaction.atomic + @action(detail=True, methods=['post']) + def pay(self, request, pk=None): + print_request = self.get_object() + + if print_request.status != 'AWAITING_PAYMENT': + return Response( + {"error": "Request not awaiting payment."}, + status=status.HTTP_400_BAD_REQUEST + ) + + price = print_request.price + if price <= 0: + return Response( + {"error": "Request price is not set."}, + status=status.HTTP_400_BAD_REQUEST + ) + + data = { + "beneficiary": request.user.id, + "amount": -price, + "operation_type": "PAYMENT", + "request": print_request.id + } + + serializer = OperationSerializer(data=data) + if serializer.is_valid(): + serializer.save(agent=request.user) + print_request.status = 'PENDING' + print_request.save() + return Response(status=201) + return Response(serializer.errors, status=400) + + @transaction.atomic + @action(detail=True, methods=['post']) + def cancel(self, request, pk=None): + print_request = self.get_object() + + allowed_cancel = ['SUBMITTED','AWAITING_PAYMENT', 'PENDING'] + + if print_request.status not in allowed_cancel: + return Response( + {"error": f"Cannot cancel for status: {print_request.status}"}, + status=status.HTTP_400_BAD_REQUEST + ) + + if print_request.status == 'PENDING': + #Operation default related_name: `operation_set` + payments = print_request.operation_set.filter( + operation_type=Operation.Type.PAYMENT + ) + + if payments.count() > 1: + return Response( + {"error": "Cannot refund"}, + status=status.HTTP_409_CONFLICT + ) + elif payments.count() == 1: + payment = payments.first() + refund_amount = abs(payment.amount) + + data = { + "beneficiary": print_request.user.id, + "amount": refund_amount, + "operation_type": "REFUND", + "request": print_request.id, + "comment": f"Refund for cancellation of request {print_request.id}" + } + + serializer = OperationSerializer(data=data) + if serializer.is_valid(): + serializer.save(agent=request.user) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + print_request.status = 'CANCELED' + print_request.save() + + return Response({"status": "Request canceled successfully"}, status=status.HTTP_200_OK) + +class AdminRequestView(viewsets.ReadOnlyModelViewSet): + serializer_class = RequestSerializer + permission_classes=[IsAdminUser] + + def get_queryset(self): + # BUREAU highest (0), project types equal (1), ADHERENT lowest (2) + qs = Request.objects.all().annotate( + priority_rank=Case( + When(user__role='BUREAU', then=Value(0)), + When(user__role='ROBOTECH', then=Value(1)), + When(user__role='AUTOTECH', then=Value(1)), + When(user__role='DRONE', then=Value(1)), + When(user__role='ADHERENT', then=Value(2)), + default=Value(3), + output_field=IntegerField(), + ) + ).order_by('priority_rank', '-created_at') + return qs + + @transaction.atomic + @action(detail=True, methods=['patch']) + def change_status(self, request, pk): + print_request = self.get_object() + new_status = request.data.get('status') + price = request.data.get('price') + current_status = str(print_request.status) + statuses = Request.Status.values + + if current_status == Request.Status.SUBMITTED and new_status and new_status != Request.Status.AWAITING_PAYMENT: + return Response( + {'error': 'New requests must move to awaiting payment first.'}, + status=status.HTTP_400_BAD_REQUEST + ) + + if current_status == Request.Status.AWAITING_PAYMENT: + if not new_status: + return Response( + {'error': 'Request is waiting for user payment.'}, + status=status.HTTP_400_BAD_REQUEST + ) + if new_status != Request.Status.AWAITING_PAYMENT: + return Response( + {'error': 'Request must be paid before advancing status.'}, + status=status.HTTP_400_BAD_REQUEST + ) + + if current_status in [Request.Status.SUBMITTED, Request.Status.AWAITING_PAYMENT] and new_status == Request.Status.AWAITING_PAYMENT: + if price is None: + return Response( + {'error': 'A price is required before setting awaiting payment.'}, + status=status.HTTP_400_BAD_REQUEST + ) + + try: + price = int(price) + except (TypeError, ValueError): + return Response( + {'error': 'Invalid price.'}, + status=status.HTTP_400_BAD_REQUEST + ) + + if price <= 0: + return Response( + {'error': 'Price must be greater than zero.'}, + status=status.HTTP_400_BAD_REQUEST + ) + + print_request.price = price + + if not new_status: + current_index = statuses.index(current_status) + + if current_status == Request.Status.SUBMITTED: + new_status = Request.Status.AWAITING_PAYMENT + else: + # Cannot autochange to ERROR/CANCELED. + ERROR_STATUS_COUNT = 2 + if current_index < len(statuses) - ERROR_STATUS_COUNT - 1: + new_status = statuses[current_index + 1] + else: + return Response( + {'error': 'Already Completed.'}, + status=status.HTTP_400_BAD_REQUEST + ) + + if new_status == Request.Status.AWAITING_PAYMENT: + if price is None: + return Response( + {'error': 'A price is required before setting awaiting payment.'}, + status=status.HTTP_400_BAD_REQUEST + ) + + try: + price = int(price) + except (TypeError, ValueError): + return Response( + {'error': 'Invalid price.'}, + status=status.HTTP_400_BAD_REQUEST + ) + + if price <= 0: + return Response( + {'error': 'Price must be greater than zero.'}, + status=status.HTTP_400_BAD_REQUEST + ) + + print_request.price = price + + if new_status not in statuses: + return Response( + {'error': f'Invalid status: {new_status}'}, + status=status.HTTP_400_BAD_REQUEST + ) + + + print_request.status = new_status + print_request.save() + + serializer = self.get_serializer(print_request) + return Response(serializer.data) + + +class AdminUserView(viewsets.ModelViewSet): + queryset = User.objects.all().order_by('email') + serializer_class = AdminUserSerializer + permission_classes = [IsAdminUser] + + +class FilamentView(viewsets.ReadOnlyModelViewSet): + queryset = Filament.objects.all() + serializer_class = FilamentSerializer + permission_classes = [permissions.AllowAny] + +class AdminFilamentView(viewsets.ModelViewSet): + queryset = Filament.objects.all() + serializer_class = FilamentSerializer + permission_classes = [permissions.IsAdminUser] + +class PrinterView(viewsets.ReadOnlyModelViewSet): + queryset = Printer.objects.all() + serializer_class = PrinterSerializer + permission_classes = [permissions.AllowAny] + +class AdminPrinterView(mixins.UpdateModelMixin, + mixins.RetrieveModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet): + queryset = Printer.objects.all() + serializer_class = PrinterSerializer + permission_classes = [permissions.IsAdminUser] \ No newline at end of file diff --git a/PrINTech-Back/__init__.py b/PrINTech-Back/back/migrations/__init__.py similarity index 100% rename from PrINTech-Back/__init__.py rename to PrINTech-Back/back/migrations/__init__.py diff --git a/PrINTech-Back/back/settings/base.py b/PrINTech-Back/back/settings/base.py index f58712a..90613e7 100644 --- a/PrINTech-Back/back/settings/base.py +++ b/PrINTech-Back/back/settings/base.py @@ -112,7 +112,7 @@ LOCAL_APPS = [ 'back', - 'back.apps.api', + 'back.apps.api.apps.ApiConfig', ] INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS diff --git a/PrINTech-Back/back/urls.py b/PrINTech-Back/back/urls.py index 62e7194..5ad2fd5 100644 --- a/PrINTech-Back/back/urls.py +++ b/PrINTech-Back/back/urls.py @@ -1,15 +1,37 @@ from django.conf import settings from django.contrib import admin from django.urls import include, path -from rest_framework import routers, permissions +from django.conf.urls.static import static +from rest_framework.routers import DefaultRouter from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView -from .apps.api.views import CreateUserView, UserMeView, ChangePasswordView -from .views import home -admin.autodiscover() +from .apps.api.views import ( + CreateUserView, + UserMeView, + ChangePasswordView, + OperationView, + AdminOperationView, + RequestView, + AdminRequestView, + AdminUserView, + FilamentView, + AdminFilamentView, + PrinterView, + AdminPrinterView, +) +from .views import home -router = routers.DefaultRouter() +router = DefaultRouter() +router.register(r'requests', RequestView, basename='request') +router.register(r'admin/requests', AdminRequestView, basename='admin-request') +router.register(r'admin/users', AdminUserView, basename='admin-user') +router.register(r'operations', OperationView, basename='operation') +router.register(r'admin/operations', AdminOperationView, basename='admin-operation') +router.register(r'filaments', FilamentView, basename='filament') +router.register(r'admin/filaments', AdminFilamentView, basename='filament-admin') +router.register(r'printers', PrinterView, basename='printer') +router.register(r'admin/printers', AdminPrinterView, basename='admin-printer') urlpatterns = [ # Examples: @@ -31,9 +53,9 @@ path('admin/', admin.site.urls), ] -# debug toolbar for dev -if settings.DEBUG and 'debug_toolbar'in settings.INSTALLED_APPS: +if settings.DEBUG and 'debug_toolbar' in settings.INSTALLED_APPS: import debug_toolbar - urlpatterns += [ - path('__debug__/', include(debug_toolbar.urls)), - ] + urlpatterns += [path('__debug__/', include(debug_toolbar.urls))] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/PrINTech-Back/back/wsgi.py b/PrINTech-Back/back/wsgi.py index d2777c2..3a57d9d 100644 --- a/PrINTech-Back/back/wsgi.py +++ b/PrINTech-Back/back/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "back.settings.{{ goal }}") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "back.settings.dev") application = get_wsgi_application() diff --git a/PrINTech-Back/manage.py b/PrINTech-Back/manage.py index 775faa1..f57e38f 100644 --- a/PrINTech-Back/manage.py +++ b/PrINTech-Back/manage.py @@ -3,8 +3,7 @@ import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "back.settings.{{ goal }}") - + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "back.settings.dev") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) diff --git a/PrINTech-Back/patch_html.html b/PrINTech-Back/patch_html.html new file mode 100644 index 0000000..c559464 --- /dev/null +++ b/PrINTech-Back/patch_html.html @@ -0,0 +1,42 @@ +
+
Historique des impressions
+
+ Debug Info: +

fullHistory length: {{ fullHistory.length }}

+

filteredHistory length: {{ filteredHistory.length }}

+

Error? {{ errorMessage || 'None' }}

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
IdModèle 3DImprimante 3DMatériauDateStatutAction
{{ item.id | slice:0:8 }}{{ getFileName(item.file?.path) }}{{ item.printer || '-' }}{{ getFilamentName(item.file?.filament) }}{{ item.created_at | date:'short' }} + {{ item.status }} +
+
+
diff --git a/README.md b/README.md index c9c6c99..e253b95 100644 Binary files a/README.md and b/README.md differ diff --git a/diagrams/db-diagram.puml b/diagrams/db-diagram.puml deleted file mode 100644 index a61ee0b..0000000 --- a/diagrams/db-diagram.puml +++ /dev/null @@ -1,127 +0,0 @@ -@startuml -title " UML Diagram \n Database" - -hide circle -skinparam linetype ortho -skinparam shadowing false -skinparam roundcorner 10 -skinparam nodesep 80 -skinparam ranksep 80 -skinparam monochrome true -skinparam backgroundColor white -skinparam defaultFontName Segoe UI, Arial, sans-serif -skinparam style strictuml - - - - - -package "User Environment" #DDDDDD{ - - enum RequestStatus { - PENDING - PROCESSING - COMPLETED - FAILED - } - enum OperationType { - CASH - CARD - OTHER - } - - entity "User" as user { - *id : UUID <> - -- - *username : varchar(150) <> - *password : varchar(128) - *email : varchar(254) <> - credit : integer = 0 - is_staff = false - is_superuser = false - ... - } - - entity "Operation" as operation{ - *id : UUID <> - -- - *beneficiary_id : UUID <> - *agent_id : UUID <> - *operation_type : OperationType - *amount : integer = 0 - comment : text - created_at : DateTime - } - entity "File" as file{ - *id : UUID <> - -- - *user_id : UUID <> - *path : varchar(100) - number_of_printing : integer = 1 {>= 0} - para_slicer : jsonb - } - - - entity "Request" as request{ - *id : UUID <> - -- - *file_id : UUID <> - *printer_id : Bigint <> - status : RequestStatus = PENDING - created_at : DateTime - } -} -package "Material Assets" #DDDDDD{ - entity "Printer" as printer { - *id : Bigint <> - -- - *type : PrinterType - *status : PrinterStatus = DOWN - } - - entity "Filament" as filament{ - *id : Bigint <> - -- - *colour : varchar(25) - *type : FilamentType - quantity : integer = 0 {>= 0} - } - - enum FilamentType { - PLA - REINFORCED_PLA - PETG - ABS - TPE/TPU - } - - enum PrinterType { - SLA - SLS - FDM - MJP - DMLS - BINDER_JETTING - } - - enum PrinterStatus { - UP - DOWN - USED - } -} - - - - -file }o-l- user : owns -file ||-r- request : linked to -"Material Assets" <--> "User Environment" : Interacts -printer ||-- request : processes -operation }o-- user : beneficiary or agent -request .d.> RequestStatus : uses -printer ..> PrinterStatus : uses -printer ..> PrinterType : uses -filament ..> FilamentType : uses -operation .r.> OperationType : uses -@enduml \ No newline at end of file diff --git a/prINTech-front/.editorconfig b/prINTech-front/.editorconfig new file mode 100644 index 0000000..f166060 --- /dev/null +++ b/prINTech-front/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single +ij_typescript_use_double_quotes = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/prINTech-front/.gitignore b/prINTech-front/.gitignore new file mode 100644 index 0000000..9c7a887 --- /dev/null +++ b/prINTech-front/.gitignore @@ -0,0 +1,52 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/prINTech-front/.prettierrc b/prINTech-front/.prettierrc new file mode 100644 index 0000000..d6c16d7 --- /dev/null +++ b/prINTech-front/.prettierrc @@ -0,0 +1,12 @@ +{ + "printWidth": 100, + "singleQuote": true, + "overrides": [ + { + "files": "*.html", + "options": { + "parser": "angular" + } + } + ] +} diff --git a/prINTech-front/.vscode/extensions.json b/prINTech-front/.vscode/extensions.json new file mode 100644 index 0000000..77b3745 --- /dev/null +++ b/prINTech-front/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/prINTech-front/.vscode/launch.json b/prINTech-front/.vscode/launch.json new file mode 100644 index 0000000..925af83 --- /dev/null +++ b/prINTech-front/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: test", + "url": "http://localhost:9876/debug.html" + } + ] +} diff --git a/prINTech-front/.vscode/mcp.json b/prINTech-front/.vscode/mcp.json new file mode 100644 index 0000000..956af8c --- /dev/null +++ b/prINTech-front/.vscode/mcp.json @@ -0,0 +1,9 @@ +{ + // For more information, visit: https://angular.dev/ai/mcp + "servers": { + "angular-cli": { + "command": "npx", + "args": ["-y", "@angular/cli", "mcp"] + } + } +} diff --git a/prINTech-front/.vscode/tasks.json b/prINTech-front/.vscode/tasks.json new file mode 100644 index 0000000..244306f --- /dev/null +++ b/prINTech-front/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "Changes detected" + }, + "endsPattern": { + "regexp": "bundle generation (complete|failed)" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "Changes detected" + }, + "endsPattern": { + "regexp": "bundle generation (complete|failed)" + } + } + } + } + ] +} diff --git a/prINTech-front/README.md b/prINTech-front/README.md new file mode 100644 index 0000000..289f90d --- /dev/null +++ b/prINTech-front/README.md @@ -0,0 +1,59 @@ +# PrINTechFront + +This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.2.3. + +## Development server + +To start a local development server, run: + +```bash +ng serve +``` + +Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. + +## Code scaffolding + +Angular CLI includes powerful code scaffolding tools. To generate a new component, run: + +```bash +ng generate component component-name +``` + +For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: + +```bash +ng generate --help +``` + +## Building + +To build the project run: + +```bash +ng build +``` + +This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. + +## Running unit tests + +To execute unit tests with the [Vitest](https://vitest.dev/) test runner, use the following command: + +```bash +ng test +``` + +## Running end-to-end tests + +For end-to-end (e2e) testing, run: + +```bash +ng e2e +``` + +Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. + +## Additional Resources + +For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. diff --git a/prINTech-front/angular.json b/prINTech-front/angular.json new file mode 100644 index 0000000..a05b0e7 --- /dev/null +++ b/prINTech-front/angular.json @@ -0,0 +1,80 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "npm", + "analytics": false + }, + "newProjectRoot": "projects", + "projects": { + "prINTech-front": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "options": { + "proxyConfig": "proxy.conf.json" + }, + "configurations": { + "production": { + "buildTarget": "prINTech-front:build:production" + }, + "development": { + "buildTarget": "prINTech-front:build:development" + } + }, + "defaultConfiguration": "development" + }, + "test": { + "builder": "@angular/build:unit-test" + } + } + } + } +} diff --git a/prINTech-front/package-lock.json b/prINTech-front/package-lock.json new file mode 100644 index 0000000..aa1d718 --- /dev/null +++ b/prINTech-front/package-lock.json @@ -0,0 +1,8740 @@ +{ + "name": "pr-intech-front", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pr-intech-front", + "version": "0.0.0", + "dependencies": { + "@angular/common": "^21.2.0", + "@angular/compiler": "^21.2.0", + "@angular/core": "^21.2.0", + "@angular/forms": "^21.2.0", + "@angular/platform-browser": "^21.2.0", + "@angular/router": "^21.2.0", + "jwt-decode": "^4.0.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0" + }, + "devDependencies": { + "@angular/build": "^21.2.3", + "@angular/cli": "^21.2.3", + "@angular/compiler-cli": "^21.2.0", + "@playwright/test": "^1.60.0", + "@types/node": "^25.9.2", + "jsdom": "^28.0.0", + "prettier": "^3.8.1", + "typescript": "~5.9.2", + "vitest": "^4.0.8" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@algolia/abtesting": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.14.1.tgz", + "integrity": "sha512-Dkj0BgPiLAaim9sbQ97UKDFHJE/880wgStAM18U++NaJ/2Cws34J5731ovJifr6E3Pv4T2CqvMXf8qLCC417Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.48.1.tgz", + "integrity": "sha512-LV5qCJdj+/m9I+Aj91o+glYszrzd7CX6NgKaYdTOj4+tUYfbS62pwYgUfZprYNayhkQpVFcrW8x8ZlIHpS23Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.48.1.tgz", + "integrity": "sha512-/AVoMqHhPm14CcHq7mwB+bUJbfCv+jrxlNvRjXAuO+TQa+V37N8k1b0ijaRBPdmSjULMd8KtJbQyUyabXOu6Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.48.1.tgz", + "integrity": "sha512-VXO+qu2Ep6ota28ktvBm3sG53wUHS2n7bgLWmce5jTskdlCD0/JrV4tnBm1l7qpla1CeoQb8D7ShFhad+UoSOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.48.1.tgz", + "integrity": "sha512-zl+Qyb0nLg+Y5YvKp1Ij+u9OaPaKg2/EPzTwKNiVyOHnQJlFxmXyUZL1EInczAZsEY8hVpPCLtNfhMhfxluXKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.48.1.tgz", + "integrity": "sha512-r89Qf9Oo9mKWQXumRu/1LtvVJAmEDpn8mHZMc485pRfQUMAwSSrsnaw1tQ3sszqzEgAr1c7rw6fjBI+zrAXTOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.48.1.tgz", + "integrity": "sha512-TPKNPKfghKG/bMSc7mQYD9HxHRUkBZA4q1PEmHgICaSeHQscGqL4wBrKkhfPlDV1uYBKW02pbFMUhsOt7p4ZpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.48.1.tgz", + "integrity": "sha512-4Fu7dnzQyQmMFknYwTiN/HxPbH4DyxvQ1m+IxpPp5oslOgz8m6PG5qhiGbqJzH4HiT1I58ecDiCAC716UyVA8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.48.1.tgz", + "integrity": "sha512-/RFq3TqtXDUUawwic/A9xylA2P3LDMO8dNhphHAUOU51b1ZLHrmZ6YYJm3df1APz7xLY1aht6okCQf+/vmrV9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.48.1.tgz", + "integrity": "sha512-Of0jTeAZRyRhC7XzDSjJef0aBkgRcvRAaw0ooYRlOw57APii7lZdq+layuNdeL72BRq1snaJhoMMwkmLIpJScw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.48.1.tgz", + "integrity": "sha512-bE7JcpFXzxF5zHwj/vkl2eiCBvyR1zQ7aoUdO+GDXxGp0DGw7nI0p8Xj6u8VmRQ+RDuPcICFQcCwRIJT5tDJFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.48.1.tgz", + "integrity": "sha512-MK3wZ2koLDnvH/AmqIF1EKbJlhRS5j74OZGkLpxI4rYvNi9Jn/C7vb5DytBnQ4KUWts7QsmbdwHkxY5txQHXVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.48.1.tgz", + "integrity": "sha512-2oDT43Y5HWRSIQMPQI4tA/W+TN/N2tjggZCUsqQV440kxzzoPGsvv9QP1GhQ4CoDa+yn6ygUsGp6Dr+a9sPPSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.48.1.tgz", + "integrity": "sha512-xcaCqbhupVWhuBP1nwbk1XNvwrGljozutEiLx06mvqDf3o8cHyEgQSHS4fKJM+UAggaWVnnFW+Nne5aQ8SUJXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.2102.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.3.tgz", + "integrity": "sha512-G4wSWUbtWp1WCKw5GMRqHH8g4m5RBpIyzt8n8IX5Pm6iYe/rwCBSKL3ktEkk7AYMwjtonkRlDtAK1GScFsf1Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.3", + "rxjs": "7.8.2" + }, + "bin": { + "architect": "bin/cli.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "21.2.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.3.tgz", + "integrity": "sha512-i++JVHOijyFckjdYqKbSXUpKnvmO2a0Utt/wQVwiLAT0O9H1hR/2NGPzubB4hnLMNSyVWY8diminaF23mZ0xjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "21.2.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.3.tgz", + "integrity": "sha512-tc/bBloRTVIBWGRiMPln1QbW+2QPj+YnWL/nG79abLKWkdrL9dJLcCRXY7dsPNrxOc/QF+8tVpnr8JofhWL9cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.3", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.21", + "ora": "9.3.0", + "rxjs": "7.8.2" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/build": { + "version": "21.2.3", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.3.tgz", + "integrity": "sha512-u4bhVQruK7KOuHQuoltqlHg+szp0f6rnsGIUolJnT3ez5V6OuSoWIxUorSbvryi2DiKRD/3iwMq7qJN1aN9HCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.2102.3", + "@babel/core": "7.29.0", + "@babel/helper-annotate-as-pure": "7.27.3", + "@babel/helper-split-export-declaration": "7.24.7", + "@inquirer/confirm": "5.1.21", + "@vitejs/plugin-basic-ssl": "2.1.4", + "beasties": "0.4.1", + "browserslist": "^4.26.0", + "esbuild": "0.27.3", + "https-proxy-agent": "7.0.6", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "listr2": "9.0.5", + "magic-string": "0.30.21", + "mrmime": "2.0.1", + "parse5-html-rewriting-stream": "8.0.0", + "picomatch": "4.0.3", + "piscina": "5.1.4", + "rolldown": "1.0.0-rc.4", + "sass": "1.97.3", + "semver": "7.7.4", + "source-map-support": "0.5.21", + "tinyglobby": "0.2.15", + "undici": "7.22.0", + "vite": "7.3.1", + "watchpack": "2.5.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "lmdb": "3.5.1" + }, + "peerDependencies": { + "@angular/compiler": "^21.0.0", + "@angular/compiler-cli": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/localize": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/platform-server": "^21.0.0", + "@angular/service-worker": "^21.0.0", + "@angular/ssr": "^21.2.3", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "^21.0.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "tslib": "^2.3.0", + "typescript": ">=5.9 <6.0", + "vitest": "^4.0.8" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, + "@angular/localize": { + "optional": true + }, + "@angular/platform-browser": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@angular/cli": { + "version": "21.2.3", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.3.tgz", + "integrity": "sha512-QzDxnSy8AUOz6ca92xfbNuEmRdWRDi1dfFkxDVr+4l6XUnA9X6VmOi7ioCO1I9oDR73LXHybOqkqHBYDlqt/Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.2102.3", + "@angular-devkit/core": "21.2.3", + "@angular-devkit/schematics": "21.2.3", + "@inquirer/prompts": "7.10.1", + "@listr2/prompt-adapter-inquirer": "3.0.5", + "@modelcontextprotocol/sdk": "1.26.0", + "@schematics/angular": "21.2.3", + "@yarnpkg/lockfile": "1.1.0", + "algoliasearch": "5.48.1", + "ini": "6.0.0", + "jsonc-parser": "3.3.1", + "listr2": "9.0.5", + "npm-package-arg": "13.0.2", + "pacote": "21.3.1", + "parse5-html-rewriting-stream": "8.0.0", + "semver": "7.7.4", + "yargs": "18.0.0", + "zod": "4.3.6" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "21.2.5", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.5.tgz", + "integrity": "sha512-MTjCbsHBkF9W12CW9yYiTJdVfZv/qCqBCZ2iqhMpDA5G+ZJiTKP0IDTJVrx2N5iHfiJ1lnK719t/9GXROtEAvg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/core": "21.2.5", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "21.2.5", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.5.tgz", + "integrity": "sha512-QloEsknGqLvmr+ED7QShDt7SoMY9mipV+gVnwn4hBI5sbl+TOBfYWXIaJMnxseFwSqjXTSCVGckfylIlynNcFg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@angular/compiler-cli": { + "version": "21.2.5", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.5.tgz", + "integrity": "sha512-Ox3vz6KAM7i47ujR/3M3NCOeCRn6vrC9yV1SHZRhSrYg6CWWcOMveavEEwtNjYtn3hOzrktO4CnuVwtDbU8pLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.29.0", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^5.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^18.0.0" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "21.2.5", + "typescript": ">=5.9 <6.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular/core": { + "version": "21.2.5", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.5.tgz", + "integrity": "sha512-JgHU134Adb1wrpyGC9ozcv3hiRAgaFTvJFn1u9OU/AVXyxu4meMmVh2hp5QhAvPnv8XQdKWWIkAY+dbpPE6zKA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "21.2.5", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0 || ~0.16.0" + }, + "peerDependenciesMeta": { + "@angular/compiler": { + "optional": true + }, + "zone.js": { + "optional": true + } + } + }, + "node_modules/@angular/forms": { + "version": "21.2.5", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.5.tgz", + "integrity": "sha512-pqRuK+a1ZAFZbs8/dZoorFJah2IWaf/SH8axHUpaDJ7fyNrwNEcpczyObdxZ00lOgORpKAhWo/q0hlVS+In8cw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "21.2.5", + "@angular/core": "21.2.5", + "@angular/platform-browser": "21.2.5", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "21.2.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.5.tgz", + "integrity": "sha512-VuuYguxjgyI4XWuoXrKynmuA3FB991pXbkNhxHeCW0yX+7DGOnGLPF1oierd4/X+IvskmN8foBZLfjyg9u4Ffg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/animations": "21.2.5", + "@angular/common": "21.2.5", + "@angular/core": "21.2.5" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/router": { + "version": "21.2.5", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.5.tgz", + "integrity": "sha512-yQGhTVGvh8OMW3auj13+g+OCSQj7gyBQON/2X4LuCvIUG71NPV6Fqzfk9DKTKaXpqo0FThy8/LPJ0Lsy3CRejg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "21.2.5", + "@angular/core": "21.2.5", + "@angular/platform-browser": "21.2.5", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz", + "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@gar/promise-retry": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", + "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@harperfast/extended-iterable": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz", + "integrity": "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz", + "integrity": "sha512-WELs+hj6xcilkloBXYf9XXK8tYEnKsgLj01Xl5ONUJpKjmT5hGVUzNUS5tooUxs7pGMrw+jFD/41WpqW4V3LDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 8", + "listr2": "9.0.5" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.1.tgz", + "integrity": "sha512-tpfN4kKrrMpQ+If1l8bhmoNkECJi0iOu6AEdrTJvWVC+32sLxTARX5Rsu579mPImRP9YFWfWgeRQ5oav7zApQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.1.tgz", + "integrity": "sha512-+a2tTfc3rmWhLAolFUWRgJtpSuu+Fw/yjn4rF406NMxhfjbMuiOUTDRvRlMFV+DzyjkwnokisskHbCWkS3Ly5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.1.tgz", + "integrity": "sha512-0EgcE6reYr8InjD7V37EgXcYrloqpxVPINy3ig1MwDSbl6LF/vXTYRH9OE1Ti1D8YZnB35ZH9aTcdfSb5lql2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.1.tgz", + "integrity": "sha512-aoERa5B6ywXdyFeYGQ1gbQpkMkDbEo45qVoXE5QpIRavqjnyPwjOulMkmkypkmsbJ5z4Wi0TBztON8agCTG0Vg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.1.tgz", + "integrity": "sha512-SqNDY1+vpji7bh0sFH5wlWyFTOzjbDOl0/kB5RLLYDAFyd/uw3n7wyrmas3rYPpAW7z18lMOi1yKlTPv967E3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-arm64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.1.tgz", + "integrity": "sha512-50v0O1Lt37cwrmR9vWZK5hRW0Aw+KEmxJJ75fge/zIYdvNKB/0bSMSVR5Uc2OV9JhosIUyklOmrEvavwNJ8D6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.1.tgz", + "integrity": "sha512-qwosvPyl+zpUlp3gRb7UcJ3H8S28XHCzkv0Y0EgQToXjQP91ZD67EHSCDmaLjtKhe+GVIW5om1KUpzVLA0l6pg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", + "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", + "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", + "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", + "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-openharmony-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@npmcli/agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/fs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", + "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "ini": "^6.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", + "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^5.0.0", + "npm-normalize-package-bin": "^5.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz", + "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz", + "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "glob": "^13.0.0", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", + "semver": "^7.5.3", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", + "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz", + "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "node-gyp": "^12.1.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.113.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.113.0.tgz", + "integrity": "sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-vRq9f4NzvbdZavhQbjkJBx7rRebDKYR9zHfO/Wg486+I7bSecdUapzCm5cyXoK+LHokTxgSq7A5baAXUZkIz0w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-kFgEvkWLqt3YCgKB5re9RlIrx9bRsvyVUnaTakEpOPuLGzLpLapYxE9BufJNvPg8GjT6mB1alN4yN1NjzoeM8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.4.tgz", + "integrity": "sha512-JXmaOJGsL/+rsmMfutcDjxWM2fTaVgCHGoXS7nE8Z3c9NAYjGqHvXrAhMUZvMpHS/k7Mg+X7n/MVKb7NYWKKww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.4.tgz", + "integrity": "sha512-ep3Catd6sPnHTM0P4hNEvIv5arnDvk01PfyJIJ+J3wVCG1eEaPo09tvFqdtcaTrkwQy0VWR24uz+cb4IsK53Qw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.4.tgz", + "integrity": "sha512-LwA5ayKIpnsgXJEwWc3h8wPiS33NMIHd9BhsV92T8VetVAbGe2qXlJwNVDGHN5cOQ22R9uYvbrQir2AB+ntT2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.4.tgz", + "integrity": "sha512-AC1WsGdlV1MtGay/OQ4J9T7GRadVnpYRzTcygV1hKnypbYN20Yh4t6O1Sa2qRBMqv1etulUknqXjc3CTIsBu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.4.tgz", + "integrity": "sha512-lU+6rgXXViO61B4EudxtVMXSOfiZONR29Sys5VGSetUY7X8mg9FCKIIjcPPj8xNDeYzKl+H8F/qSKOBVFJChCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.4.tgz", + "integrity": "sha512-DZaN1f0PGp/bSvKhtw50pPsnln4T13ycDq1FrDWRiHmWt1JeW+UtYg9touPFf8yt993p8tS2QjybpzKNTxYEwg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.4.tgz", + "integrity": "sha512-RnGxwZLN7fhMMAItnD6dZ7lvy+TI7ba+2V54UF4dhaWa/p8I/ys1E73KO6HmPmgz92ZkfD8TXS1IMV8+uhbR9g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-6lcI79+X8klGiGd8yHuTgQRjuuJYNggmEml+RsyN596P23l/zf9FVmJ7K0KVKkFAeYEdg0iMUKyIxiV5vebDNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.4.tgz", + "integrity": "sha512-wz7ohsKCAIWy91blZ/1FlpPdqrsm1xpcEOQVveWoL6+aSPKL4VUcoYmmzuLTssyZxRpEwzuIxL/GDsvpjaBtOw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.4.tgz", + "integrity": "sha512-cfiMrfuWCIgsFmcVG0IPuO6qTRHvF7NuG3wngX1RZzc6dU8FuBFb+J3MIR5WrdTNozlumfgL4cvz+R4ozBCvsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.4.tgz", + "integrity": "sha512-p6UeR9y7ht82AH57qwGuFYn69S6CZ7LLKdCKy/8T3zS9VTrJei2/CGsTUV45Da4Z9Rbhc7G4gyWQ/Ioamqn09g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.4.tgz", + "integrity": "sha512-1BrrmTu0TWfOP1riA8uakjFc9bpIUGzVKETsOtzY39pPga8zELGDl8eu1Dx7/gjM5CAz14UknsUMpBO8L+YntQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@schematics/angular": { + "version": "21.2.3", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.3.tgz", + "integrity": "sha512-rCEprgpNbJLl9Rm/t92eRYc1eIqD4BAJqB1OO8fzQolyDajCcOBpohjXkuLYSwK9RMyS6f+szNnYGOQawlrPYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.3", + "@angular-devkit/schematics": "21.2.3", + "jsonc-parser": "3.3.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sigstore/bundle": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", + "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.2.0.tgz", + "integrity": "sha512-kxHrDQ9YgfrWUSXU0cjsQGv8JykOFZQ9ErNKbFPWzk3Hgpwu8x2hHrQ9IdA8yl+j9RTLTC3sAF3Tdq1IQCP4oA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.0.tgz", + "integrity": "sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.1.tgz", + "integrity": "sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gar/promise-retry": "^1.0.2", + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.4", + "proc-log": "^6.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.2.tgz", + "integrity": "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.0.tgz", + "integrity": "sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", + "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^10.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz", + "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz", + "integrity": "sha512-HXciTXN/sDBYWgeAD4V4s0DN0g72x5mlxQhHxtYu3Tt8BLa6MzcJZUyDVFCdtjNs3bfENVHVzOsmooTVuNgAAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "chai": "^6.2.2", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.0", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/algoliasearch": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.48.1.tgz", + "integrity": "sha512-Rf7xmeuIo7nb6S4mp4abW2faW8DauZyE2faBIKFaUfP3wnpOvNSbiI5AwVhqBNj0jPgBWEvhyCu0sLjN2q77Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.14.1", + "@algolia/client-abtesting": "5.48.1", + "@algolia/client-analytics": "5.48.1", + "@algolia/client-common": "5.48.1", + "@algolia/client-insights": "5.48.1", + "@algolia/client-personalization": "5.48.1", + "@algolia/client-query-suggestions": "5.48.1", + "@algolia/client-search": "5.48.1", + "@algolia/ingestion": "1.48.1", + "@algolia/monitoring": "1.48.1", + "@algolia/recommend": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/beasties": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.1.tgz", + "integrity": "sha512-2Imdcw3LznDuxAbJM26RHniOLAzE6WgrK8OuvVXCQtNBS8rsnD9zsSEa3fHl4hHpUY7BYTlrpvtPVbvu9G6neg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "css-select": "^6.0.0", + "css-what": "^7.0.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^10.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3", + "postcss-safe-parser": "^7.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz", + "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssstyle": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz", + "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.28", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", + "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", + "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", + "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore-walk": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", + "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/immutable": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", + "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.31", + "@asamuzakjp/dom-selector": "^6.8.1", + "@bramus/specificity": "^2.4.2", + "@exodus/bytes": "^1.11.0", + "cssstyle": "^6.0.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "undici": "^7.21.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", + "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lmdb": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.1.tgz", + "integrity": "sha512-NYHA0MRPjvNX+vSw8Xxg6FLKxzAG+e7Pt8RqAQA/EehzHVXq9SxDqJIN3JL1hK0dweb884y8kIh6rkWvPyg9Wg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@harperfast/extended-iterable": "^1.0.3", + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.5.1", + "@lmdb/lmdb-darwin-x64": "3.5.1", + "@lmdb/lmdb-linux-arm": "3.5.1", + "@lmdb/lmdb-linux-arm64": "3.5.1", + "@lmdb/lmdb-linux-x64": "3.5.1", + "@lmdb/lmdb-win32-arm64": "3.5.1", + "@lmdb/lmdb-win32-x64": "3.5.1" + } + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-fetch-happen": { + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.5.tgz", + "integrity": "sha512-uCbIa8jWWmQZt4dSnEStkVC6gdakiinAm4PiGsywIkguF0eWMdcjDz0ECYhUolFU3pFLOev9VNPCEygydXnddg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/agent": "^4.0.0", + "@npmcli/redact": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.7.2" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.9.tgz", + "integrity": "sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw==", + "dev": true, + "license": "MIT", + "optional": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-gyp": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz", + "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^15.0.0", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-bundled": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", + "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-install-checks": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz", + "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", + "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-package-arg": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", + "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-packlist": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", + "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^8.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", + "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "npm-package-arg": "^13.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", + "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^4.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^15.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz", + "integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.1", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ordered-binary": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", + "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pacote": { + "version": "21.3.1", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.3.1.tgz", + "integrity": "sha512-O0EDXi85LF4AzdjG74GUwEArhdvawi/YOHcsW6IijKNj7wm8IvEWNF5GnfuxNpQ/ZpO3L37+v8hqdVh8GgWYhg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^4.0.0", + "ssri": "^13.0.0", + "tar": "^7.4.3" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.0.tgz", + "integrity": "sha512-wzh11mj8KKkno1pZEu+l2EVeWsuKDfR5KNWZOTsslfUX8lPDZx77m9T0kIoAVkFtD1nx6YF8oh4BnPHvxMtNMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0", + "parse5": "^8.0.0", + "parse5-sax-parser": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz", + "integrity": "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/piscina": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.1.4.tgz", + "integrity": "sha512-7uU4ZnKeQq22t9AsmHGD2w4OYQGonwFnTypDypaWi7Qr2EvQIFVtG8J5D/3bE7W123Wdc9+v4CZDu5hJXVCtBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.x" + }, + "optionalDependencies": { + "@napi-rs/nice": "^1.0.4" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.4.tgz", + "integrity": "sha512-V2tPDUrY3WSevrvU2E41ijZlpF+5PbZu4giH+VpNraaadsJGHa4fR6IFwsocVwEXDoAdIv5qgPPxgrvKAOIPtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.113.0", + "@rolldown/pluginutils": "1.0.0-rc.4" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.4", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.4", + "@rolldown/binding-darwin-x64": "1.0.0-rc.4", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.4", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.4", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.4", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.4", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.4", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.4", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.4", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.4", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.4" + } + }, + "node_modules/rollup": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.0.tgz", + "integrity": "sha512-/fUgUhYghuLzVT/gaJoeVehLCgZiUxPCPMcyVNY0lIf/cTCz58K/WTI7PefDarXxp9nUKpEwg1yyz3eSBMTtgA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.1.0", + "@sigstore/tuf": "^4.0.1", + "@sigstore/verify": "^3.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/ssri": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", + "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.1.tgz", + "integrity": "sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tar": { + "version": "7.5.12", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.12.tgz", + "integrity": "sha512-9TsuLcdhOn4XztcQqhNyq1KOwOOED/3k58JAvtULiYqbO8B/0IBAAIE1hj0Svmm58k27TmcigyDI0deMlgG3uw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz", + "integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.27" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz", + "integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tuf-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", + "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/prINTech-front/package.json b/prINTech-front/package.json new file mode 100644 index 0000000..f1b678f --- /dev/null +++ b/prINTech-front/package.json @@ -0,0 +1,35 @@ +{ + "name": "pr-intech-front", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "packageManager": "npm@11.12.0", + "dependencies": { + "@angular/common": "^21.2.0", + "@angular/compiler": "^21.2.0", + "@angular/core": "^21.2.0", + "@angular/forms": "^21.2.0", + "@angular/platform-browser": "^21.2.0", + "@angular/router": "^21.2.0", + "jwt-decode": "^4.0.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0" + }, + "devDependencies": { + "@angular/build": "^21.2.3", + "@angular/cli": "^21.2.3", + "@angular/compiler-cli": "^21.2.0", + "@playwright/test": "^1.60.0", + "@types/node": "^25.9.2", + "jsdom": "^28.0.0", + "prettier": "^3.8.1", + "typescript": "~5.9.2", + "vitest": "^4.0.8" + } +} diff --git a/prINTech-front/patch_history.ts b/prINTech-front/patch_history.ts new file mode 100644 index 0000000..30db936 --- /dev/null +++ b/prINTech-front/patch_history.ts @@ -0,0 +1,87 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnInit, inject, ChangeDetectorRef } from '@angular/core'; +import { HistoryServices, Filament } from '../services/history-services'; +import { HistoryItem } from './history.model'; + +@Component({ + selector: 'app-history', + imports: [CommonModule], + templateUrl: './history.html', + styleUrl: './history.css', +}) +export class History implements OnInit{ + private readonly historyService = inject(HistoryServices); + private readonly cdr = inject(ChangeDetectorRef); + + fullHistory: HistoryItem[] = []; + filteredHistory: HistoryItem[] = []; + filaments: Filament[] = []; + SearchQuery = ''; + + ngOnInit(): void { + console.log("HISTORY COMPONENT INIT"); + this.loadFilaments(); + this.loadHistory(); + } + + onSearch(event: Event) { + this.SearchQuery = (event.target as HTMLInputElement).value; + this.applyFilters(); + } + + private applyFilters(): void { + const query = this.SearchQuery.toLowerCase(); + if (!query) { + this.filteredHistory = this.fullHistory; + } else { + this.filteredHistory = this.fullHistory.filter(item => { + const fileName = this.getFileName(item.file?.path).toLowerCase(); + return fileName.includes(query); + }); + } + console.log("FILTERED HISTORY:", this.filteredHistory); + this.cdr.detectChanges(); + } + + private loadFilaments(): void { + this.historyService.getFilaments().subscribe({ + next: (filaments) => { + this.filaments = filaments; + this.cdr.detectChanges(); + }, + error: (err) => { + console.error("ERROR LOADING FILAMENTS:", err); + this.filaments = []; + }, + }); + } + + private loadHistory(): void { + this.historyService.getHistory().subscribe({ + next: (response: any) => { + console.log("RAW HISTORY RESPONSE:", response); + const items = response?.results ? response.results : (Array.isArray(response) ? response : []); + console.log("PROCESSED ITEMS:", items); + this.fullHistory = items; + this.applyFilters(); + }, + error: (err) => { + console.error("ERROR LOADING HISTORY:", err); + this.fullHistory = []; + this.applyFilters(); + }, + }); + } + + getFilamentName(filamentId: number | null | undefined): string { + if (filamentId == null) return '-'; + const filament = this.filaments.find(f => f.id === filamentId); + if (!filament) return `ID: ${filamentId}`; + return `${filament.type} (${filament.color_name})`; + } + + getFileName(path: string | null | undefined): string { + if (!path) return '-'; + return path.split('/').pop() || path; + } +} diff --git a/prINTech-front/playwright.config.ts b/prINTech-front/playwright.config.ts new file mode 100644 index 0000000..73aba40 --- /dev/null +++ b/prINTech-front/playwright.config.ts @@ -0,0 +1,79 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('')`. */ + baseURL: 'http://localhost:4200', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'npm run start', + url: 'http://localhost:4200', + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, // Give Angular build engine enough time to compile on first run +},}); diff --git a/prINTech-front/proxy.conf.json b/prINTech-front/proxy.conf.json new file mode 100644 index 0000000..8ac9bc3 --- /dev/null +++ b/prINTech-front/proxy.conf.json @@ -0,0 +1,14 @@ +{ + "/api": { + "target": "http://localhost:8000", + "secure": false, + "changeOrigin": true, + "logLevel": "debug" + }, + "/media": { + "target": "http://localhost:8000", + "secure": false, + "changeOrigin": true, + "logLevel": "debug" + } +} diff --git a/prINTech-front/public/crealityk1c-removebg-preview.png b/prINTech-front/public/crealityk1c-removebg-preview.png new file mode 100644 index 0000000..f94388e Binary files /dev/null and b/prINTech-front/public/crealityk1c-removebg-preview.png differ diff --git a/prINTech-front/public/crealityk1c.jpg b/prINTech-front/public/crealityk1c.jpg new file mode 100644 index 0000000..52d2c7c Binary files /dev/null and b/prINTech-front/public/crealityk1c.jpg differ diff --git a/prINTech-front/public/favicon.ico b/prINTech-front/public/favicon.ico new file mode 100644 index 0000000..57614f9 Binary files /dev/null and b/prINTech-front/public/favicon.ico differ diff --git a/prINTech-front/public/logo.png b/prINTech-front/public/logo.png new file mode 100644 index 0000000..507b63e Binary files /dev/null and b/prINTech-front/public/logo.png differ diff --git a/prINTech-front/public/profile.png b/prINTech-front/public/profile.png new file mode 100644 index 0000000..7c867c6 Binary files /dev/null and b/prINTech-front/public/profile.png differ diff --git a/prINTech-front/public/profile.svg b/prINTech-front/public/profile.svg new file mode 100644 index 0000000..f42b4f9 --- /dev/null +++ b/prINTech-front/public/profile.svg @@ -0,0 +1,52 @@ + + + + + + + + + + diff --git a/prINTech-front/public/prusa-i3-mk3-removebg-preview.png b/prINTech-front/public/prusa-i3-mk3-removebg-preview.png new file mode 100644 index 0000000..d65fdb7 Binary files /dev/null and b/prINTech-front/public/prusa-i3-mk3-removebg-preview.png differ diff --git a/prINTech-front/public/prusa-i3-mk3.jpg b/prINTech-front/public/prusa-i3-mk3.jpg new file mode 100644 index 0000000..d9af465 Binary files /dev/null and b/prINTech-front/public/prusa-i3-mk3.jpg differ diff --git a/prINTech-front/public/snapmaker u1.png b/prINTech-front/public/snapmaker u1.png new file mode 100644 index 0000000..4240df7 Binary files /dev/null and b/prINTech-front/public/snapmaker u1.png differ diff --git a/prINTech-front/public/snapmaker-u1.png b/prINTech-front/public/snapmaker-u1.png new file mode 100644 index 0000000..dbaa6f8 Binary files /dev/null and b/prINTech-front/public/snapmaker-u1.png differ diff --git a/prINTech-front/src/app/admin-dashboard/admin-dashboard.css b/prINTech-front/src/app/admin-dashboard/admin-dashboard.css new file mode 100644 index 0000000..b2dde95 --- /dev/null +++ b/prINTech-front/src/app/admin-dashboard/admin-dashboard.css @@ -0,0 +1,318 @@ +/* ── Reset & Base ─────────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +:host { + --navy: #1B3A6B; + --navy-dark: #122850; + --accent: #2563EB; + --accent-lt: #DBEAFE; + --surface: #F8FAFC; + --card: #FFFFFF; + --border: #E2E8F0; + --text: #0F172A; + --muted: #64748B; + --danger: #DC2626; + --danger-lt: #FEE2E2; + --success: #16A34A; + --success-lt:#DCFCE7; + --warn: #D97706; + --warn-lt: #FEF3C7; + --radius: 10px; + --shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.05); +} + +body { font-family: 'Segoe UI', system-ui, sans-serif; background: var(--surface); color: var(--text); } + +/* ── Layout ───────────────────────────────────────────────────── */ +.page-content { + max-width: 1200px; + margin: 0 auto; + padding: 24px 20px; + display: flex; + flex-direction: column; + gap: 20px; +} + +.page-header { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; +} + +.title { + font-size: 26px; + font-weight: 800; + color: var(--navy); + letter-spacing: -0.5px; +} + +/* ── Tabs ─────────────────────────────────────────────────────── */ +.tabs { + display: flex; + gap: 4px; + border-bottom: 2px solid var(--border); + overflow-x: auto; + scrollbar-width: none; +} +.tabs::-webkit-scrollbar { display: none; } + +.tab-btn { + background: none; + color: var(--muted); + border: none; + border-radius: var(--radius) var(--radius) 0 0; + padding: 10px 18px; + font-size: 0.88rem; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + border-bottom: 2px solid transparent; + margin-bottom: -2px; + transition: color .15s, border-color .15s; +} +.tab-btn:hover { color: var(--navy); } +.tab-btn.active { + color: var(--accent); + border-bottom-color: var(--accent); + background: var(--card); +} + +.tab-panel { display: none; } +.tab-panel.active { display: flex; flex-direction: column; gap: 20px; } + +/* ── Cards / Sections ─────────────────────────────────────────── */ +.section { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 18px; + box-shadow: var(--shadow); +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 14px; +} + +.section-header h2 { + font-size: 1rem; + font-weight: 700; + color: var(--navy); +} + +/* ── Alerts ───────────────────────────────────────────────────── */ +.alert { + border-radius: var(--radius); + padding: 10px 14px; + font-size: 0.9rem; + font-weight: 500; +} +.alert-error { background: var(--danger-lt); color: var(--danger); border: 1px solid #FECACA; } +.alert-success { background: var(--success-lt); color: var(--success); border: 1px solid #BBF7D0; } + +/* ── Tables ───────────────────────────────────────────────────── */ +.table-wrapper { overflow-x: auto; } + +table { + width: 100%; + border-collapse: collapse; + font-size: 0.87rem; +} + +thead { background: var(--surface); } + +th, td { + text-align: left; + padding: 10px 12px; + border-bottom: 1px solid var(--border); + vertical-align: middle; +} + +th { + font-weight: 700; + color: var(--muted); + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: .04em; +} + +th.sortable { cursor: pointer; user-select: none; } +th.sortable:hover { color: var(--navy); } + +tbody tr:last-child td { border-bottom: none; } +tbody tr:hover { background: #F1F5F9; } + +/* ── Search ───────────────────────────────────────────────────── */ +.search-row { margin-bottom: 12px; } + +/* ── Forms ────────────────────────────────────────────────────── */ +.form-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 10px; + align-items: end; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 4px; +} + +.form-group label { + font-size: 0.78rem; + font-weight: 600; + color: var(--muted); + text-transform: uppercase; + letter-spacing: .03em; +} + +.form-full { grid-column: 1 / -1; } + +.form-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; + align-items: center; + margin-top: 4px; +} + +/* ── Inputs ───────────────────────────────────────────────────── */ +input[type="text"], +input[type="email"], +input[type="password"], +input[type="number"], +input[type="search"], +input[type="color"], +select, +textarea { + padding: 9px 11px; + border: 1px solid var(--border); + border-radius: 8px; + font-size: 0.88rem; + background: var(--card); + color: var(--text); + width: 100%; + transition: border-color .15s, box-shadow .15s; +} +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(37,99,235,.12); +} + +input[type="color"] { + padding: 4px 6px; + height: 38px; + cursor: pointer; +} + +input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + accent-color: var(--accent); +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.88rem; + cursor: pointer; +} + +/* ── Buttons ──────────────────────────────────────────────────── */ +button { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 14px; + border: none; + border-radius: 8px; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + transition: opacity .15s, background .15s; + background: var(--navy); + color: #fff; +} +button:hover { opacity: .88; } +button:disabled { opacity: .45; cursor: not-allowed; } + +button.btn-primary { background: var(--accent) !important; color: #fff !important; } +button.btn-navy { background: var(--navy) !important; color: #fff !important; } +button.btn-danger { background: var(--danger) !important; color: #fff !important; } +button.btn-ghost { background: var(--surface) !important; color: var(--text) !important; border: 1px solid var(--border); } +button.btn-sm { padding: 5px 10px; font-size: 0.8rem; } +button.btn-icon { padding: 6px 8px; } + +/* ── Badges ───────────────────────────────────────────────────── */ +.badge { + display: inline-block; + border-radius: 999px; + padding: 3px 10px; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: .02em; +} + +.badge-queued { background: var(--accent-lt); color: #1E40AF; } +.badge-progress { background: var(--warn-lt); color: #92400E; } +.badge-done { background: var(--success-lt);color: #166534; } +.badge-failed { background: var(--danger-lt); color: #991B1B; } + +.badge-up { background: var(--success-lt); color: var(--success); } +.badge-down { background: var(--danger-lt); color: var(--danger); } +.badge-used { background: var(--warn-lt); color: var(--warn); } + +/* Role badges */ +.badge-role-BUREAU { background: #EDE9FE; color: #5B21B6; } +.badge-role-ROBOTECH { background: #DBEAFE; color: #1E40AF; } +.badge-role-AUTOTECH { background: #D1FAE5; color: #065F46; } +.badge-role-DRONE { background: #FEF3C7; color: #92400E; } +.badge-role-ADHERENT { background: #F1F5F9; color: #475569; } + +/* ── Color swatch ─────────────────────────────────────────────── */ +.color-swatch { + display: inline-flex; + align-items: center; + gap: 8px; +} +.swatch { + display: inline-block; + width: 16px; + height: 16px; + border-radius: 4px; + border: 1px solid var(--border); + flex-shrink: 0; +} + +/* ── Section highlight ────────────────────────────────────────── */ +.section.editing { + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(37,99,235,.1); +} + +/* ── Divider ──────────────────────────────────────────────────── */ +.divider { + border: none; + border-top: 1px solid var(--border); + margin: 12px 0; +} + +/* ── Empty state ──────────────────────────────────────────────── */ +.empty-cell { color: var(--muted); font-style: italic; } + +/* ── Responsive ───────────────────────────────────────────────── */ +@media (max-width: 640px) { + .form-grid { grid-template-columns: 1fr; } + .tab-btn { padding: 8px 12px; } +} \ No newline at end of file diff --git a/prINTech-front/src/app/admin-dashboard/admin-dashboard.html b/prINTech-front/src/app/admin-dashboard/admin-dashboard.html new file mode 100644 index 0000000..f0e9b02 --- /dev/null +++ b/prINTech-front/src/app/admin-dashboard/admin-dashboard.html @@ -0,0 +1,449 @@ +
+ + + + + + @if (errorMessage()) { +
{{ errorMessage() }}
+ } + @if (successMessage()) { +
{{ successMessage() }}
+ } + + + + + + + +
+
+
+

Impressions

+ Cliquez un en-tête pour trier +
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Id {{ sortIndicator('id') }}Fichier {{ sortIndicator('file') }}Utilisateur {{ sortIndicator('user') }}PrioritéDate {{ sortIndicator('created_at') }}Statut {{ sortIndicator('status') }}Changer statutPrix (€)Téléchargement
Aucune impression en attente.
{{ item.id | slice:0:8 }}…{{ getFileName(item.file?.path) }}{{ getUserEmail(item.user) }}{{ getUserRole(item.user) }}{{ item.created_at | date:'dd/MM/yy HH:mm' }}{{ item.status }} +
+ + @if (getPendingStatus(item.id, item.status) === 'AWAITING_PAYMENT') { + + } + @if (hasPendingStatusChange(item.id, item.status)) { + + } +
+
+ @if (item.price) { {{ item.price }} } + @else { } + + @if (item.file?.path) { + ⬇ Télécharger + } @else { + + } +
+
+
+
+ + + + +
+ + +
+

Ajouter un compte

+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+ +
+
+
+ + +
+

Comptes utilisateurs

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
UsernameEmailCréditPrioritéAdminActifActions
Aucun utilisateur.
{{ user.username }}{{ user.email }}{{ user.credit }}{{ getUserRole(user.id) }}{{ user.is_staff ? 'Oui' : 'Non' }}{{ user.is_active ? 'Oui' : 'Non' }} +
+ + +
+
+
+
+ + +
+
+

{{ editingUserId() ? 'Modifier le compte' : 'Modifier un compte' }}

+ @if (editingUserId()) { + + } +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+ +
+
+
+ +
+ + + + +
+ + +
+

Nouvelle opération

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
+ + +
+

Historique des opérations

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdBénéficiaireAgentMontantTypeDemandeDateCommentaire
Aucune opération.
{{ op.id | slice:0:8 }}…{{ getUserEmail(op.beneficiary) }}{{ getUserEmail(op.agent) }} + {{ op.amount >= 0 ? '+' : '' }}{{ op.amount }} + {{ op.operation_type }}{{ op.request ? (op.request | slice:0:8) + '…' : '—' }}{{ op.created_at | date:'dd/MM/yy HH:mm' }}{{ op.comment || '—' }}
+
+
+ +
+ + + + +
+ + +
+
+

{{ editingFilamentId() ? 'Modifier le filament' : 'Ajouter un filament' }}

+ @if (editingFilamentId()) { + + } +
+
+
+
+ + +
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
+ + +
+

Filaments disponibles

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
IdCouleurNomTypeQuantitéPrixActions
Aucun filament.
{{ f.id }} +
+ + {{ f.color }} +
+
{{ f.color_name }}{{ f.type }}{{ f.quantity }} g{{ f.price }} +
+ + +
+
+
+
+ +
+ + + + +
+
+

Imprimantes

+
+ + + + + + + + + + + + + + + + + + +
NomStatutChanger statut
Aucune imprimante.
{{ printer.name }} + {{ printer.status }} + + +
+
+
+
+
\ No newline at end of file diff --git a/prINTech-front/src/app/admin-dashboard/admin-dashboard.ts b/prINTech-front/src/app/admin-dashboard/admin-dashboard.ts new file mode 100644 index 0000000..013855f --- /dev/null +++ b/prINTech-front/src/app/admin-dashboard/admin-dashboard.ts @@ -0,0 +1,526 @@ +import { CommonModule, DatePipe, SlicePipe } from '@angular/common'; +import { Component, OnInit, inject, signal } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { + AdminService, + AdminUser, + AdminUserCreatePayload, + AdminUserUpdatePayload, + PrintRequestStatus, + PrintRequest, + Filament, + FilamentPayload, + Printer, + PrinterStatus, + Operation, + OperationCreatePayload, +} from '../services/admin'; + +type TabId = 'requests' | 'users' | 'operations' | 'filaments' | 'printers'; + +@Component({ + selector: 'app-admin-dashboard', + imports: [CommonModule, ReactiveFormsModule, DatePipe, SlicePipe], + templateUrl: './admin-dashboard.html', + styleUrl: './admin-dashboard.css', +}) +export class AdminDashboard implements OnInit { + private readonly adminService = inject(AdminService); + private readonly formBuilder = inject(FormBuilder); + + // Tab + readonly activeTab = signal('requests'); + + setTab(tab: TabId): void { + this.activeTab.set(tab); + this.errorMessage.set(''); + this.successMessage.set(''); + } + + // Global state + readonly loading = signal(false); + readonly errorMessage = signal(''); + readonly successMessage = signal(''); + + // Requests + readonly waitingPrints = signal([]); + readonly sortedWaitingPrints = signal([]); + readonly sortColumn = signal<'id' | 'file' | 'user' | 'created_at' | 'status'>('created_at'); + readonly sortDirection = signal<'asc' | 'desc'>('desc'); + readonly searchQuery = signal(''); + updatingRequestIds = new Set(); + readonly pendingStatus = signal>({}); + pendingPrice: Record = {}; + + readonly statusOptions: PrintRequestStatus[] = [ + 'SUBMITTED', + 'AWAITING_PAYMENT', + 'PENDING', + 'PRINTING', + 'AWAITING_PICKUP', + 'PICKED_UP', + 'FAILED', + 'CANCELED', + ]; + + // Users + readonly users = signal([]); + readonly editingUserId = signal(null); + + readonly roleOptions = ['ADHERENT', 'ROBOTECH', 'AUTOTECH', 'DRONE', 'BUREAU']; + + readonly createUserForm = this.formBuilder.nonNullable.group({ + username: ['', [Validators.required]], + email: ['', [Validators.required, Validators.email]], + password: ['', [Validators.required, Validators.minLength(8)]], + is_staff: [false], + is_active: [true], + }); + + readonly editUserForm = this.formBuilder.nonNullable.group({ + id: ['', [Validators.required]], + username: [''], + email: ['', [Validators.required, Validators.email]], + password: [''], + role: ['ADHERENT'], + is_staff: [false], + is_active: [true], + }); + + // Operations + readonly operations = signal([]); + + readonly operationTypes = ['CASH', 'CARD', 'PAYMENT', 'REFUND']; + + readonly createOperationForm = this.formBuilder.nonNullable.group({ + beneficiary: ['', [Validators.required]], + amount: [0, [Validators.required]], + operation_type: ['CASH', [Validators.required]], + request: [''], + comment: [''], + }); + + // Filaments + readonly filaments = signal([]); + readonly editingFilamentId = signal(null); + + readonly filamentForm = this.formBuilder.nonNullable.group({ + color: ['#ffffff', [Validators.required]], + color_name: ['', [Validators.required]], + type: ['PLA', [Validators.required]], + quantity: [0, [Validators.required, Validators.min(0)]], + price: [0, [Validators.required, Validators.min(0)]], + }); + + // Printers + readonly printers = signal([]); + updatingPrinterNames = new Set(); + + readonly printerStatuses: PrinterStatus[] = ['UP', 'DOWN', 'USED']; + + // Lifecycle + ngOnInit(): void { + this.loadAll(); + } + + loadAll(): void { + this.loadRequests(); + this.loadUsers(); + this.loadOperations(); + this.loadFilaments(); + this.loadPrinters(); + } + + // Requests + private loadRequests(): void { + this.adminService.getWaitingRequests().subscribe({ + next: (requests) => { + /* + const waiting = requests.filter((r) => + ['SUBMITTED', 'AWAITING_PAYMENT', 'PENDING', 'PRINTING', 'AWAITING_PICKUP'].includes(r.status) + ); + */ + this.pendingStatus.set({}); + this.waitingPrints.set(requests); + this.applyPrintSort(); + }, + error: () => this.errorMessage.set('Impossible de charger les impressions en attente.'), + }); + } + + onHeaderSort(column: 'id' | 'file' | 'user' | 'created_at' | 'status'): void { + if (this.sortColumn() === column) { + this.sortDirection.set(this.sortDirection() === 'asc' ? 'desc' : 'asc'); + } else { + this.sortColumn.set(column); + this.sortDirection.set('asc'); + } + this.applyPrintSort(); + } + + sortIndicator(column: 'id' | 'file' | 'user' | 'created_at' | 'status'): string { + if (this.sortColumn() !== column) return ''; + return this.sortDirection() === 'asc' ? '↑' : '↓'; + } + + onSearchChange(event: Event): void { + this.searchQuery.set((event.target as HTMLInputElement).value.trim().toLowerCase()); + this.applyPrintSort(); + } + + onPriceInput(requestId: string, event: Event): void { + const val = parseInt((event.target as HTMLInputElement).value, 10); + if (!isNaN(val)) { + this.pendingPrice[requestId] = val; + } + } + + getPendingStatus(requestId: string, currentStatus: PrintRequestStatus): PrintRequestStatus { + return this.pendingStatus()[requestId] ?? currentStatus; + } + + hasPendingStatusChange(requestId: string, currentStatus: PrintRequestStatus): boolean { + const pending = this.pendingStatus()[requestId]; + return !!pending && pending !== currentStatus; + } + + onSelectStatus(requestId: string, event: Event): void { + const selected = (event.target as HTMLSelectElement).value as PrintRequestStatus; + this.pendingStatus.update(map => ({ ...map, [requestId]: selected })); + } + + onApplyStatus(item: PrintRequest): void { + const newStatus = this.pendingStatus()[item.id]; + if (!newStatus || newStatus === item.status || !this.canTransition(item.status, newStatus)) return; + if (this.updatingRequestIds.has(item.id)) return; + + const needsPrice = newStatus === 'AWAITING_PAYMENT'; + const price = needsPrice ? this.pendingPrice[item.id] : undefined; + + if (needsPrice && (!price || price < 0)) { + this.errorMessage.set('Un prix est requis pour passer en AWAITING_PAYMENT.'); + return; + } + + this.updatingRequestIds.add(item.id); + this.errorMessage.set(''); + + this.adminService.changeRequestStatus(item.id, newStatus, price).subscribe({ + next: () => { + this.successMessage.set('Statut mis à jour.'); + this.pendingStatus.update(map => { const m = { ...map }; delete m[item.id]; return m; }); + delete this.pendingPrice[item.id]; + this.updatingRequestIds.delete(item.id); + this.loadRequests(); + }, + error: (err) => { + this.errorMessage.set(this.readError(err, 'Impossible de changer le statut.')); + this.updatingRequestIds.delete(item.id); + this.applyPrintSort(); + }, + }); + } + + isUpdatingRequest(requestId: string): boolean { + return this.updatingRequestIds.has(requestId); + } + + canTransition(current: PrintRequestStatus, next: PrintRequestStatus): boolean { + if (current === next) return true; + const transitions: Record = { + SUBMITTED: ['AWAITING_PAYMENT', 'FAILED', 'CANCELED'], + AWAITING_PAYMENT: ['PENDING', 'FAILED', 'CANCELED'], + PENDING: ['PRINTING', 'FAILED', 'CANCELED'], + PRINTING: ['AWAITING_PICKUP', 'FAILED'], + AWAITING_PICKUP: ['PICKED_UP', 'FAILED'], + PICKED_UP: [], + FAILED: [], + CANCELED: [], + }; + return transitions[current].includes(next); + } + + getFileName(path?: string | null): string { + if (!path) return '-'; + return path.split('/').pop() || path; + } + + getDownloadUrl(path?: string | null): string { + if (!path) return ''; + if (path.startsWith('http://') || path.startsWith('https://')) return path; + return path.startsWith('/') ? path : `/${path}`; + } + + getStatusBadgeClass(status: PrintRequestStatus): string { + if (['SUBMITTED', 'AWAITING_PAYMENT', 'PENDING'].includes(status)) return 'badge-queued'; + if (['PRINTING', 'AWAITING_PICKUP'].includes(status)) return 'badge-progress'; + if (status === 'PICKED_UP') return 'badge-done'; + return 'badge-failed'; + } + + private applyPrintSort(): void { + const query = this.searchQuery(); + const filtered = !query + ? this.waitingPrints() + : this.waitingPrints().filter((item) => { + const id = item.id.toLowerCase(); + const file = this.getFileName(item.file?.path).toLowerCase(); + const user = item.user.toLowerCase(); + const st = item.status.toLowerCase(); + return id.includes(query) || file.includes(query) || user.includes(query) || st.includes(query); + }); + + const sorted = [...filtered]; + const column = this.sortColumn(); + const direction = this.sortDirection(); + + sorted.sort((a, b) => { + let result = 0; + if (column === 'created_at') { + result = new Date(a.created_at).getTime() - new Date(b.created_at).getTime(); + } else if (column === 'status') { + result = a.status.localeCompare(b.status); + } else if (column === 'user') { + result = a.user.localeCompare(b.user); + } else if (column === 'file') { + result = this.getFileName(a.file?.path).localeCompare(this.getFileName(b.file?.path)); + } else { + result = a.id.localeCompare(b.id); + } + return direction === 'asc' ? result : -result; + }); + + this.sortedWaitingPrints.set(sorted); + } + + // Users + private loadUsers(): void { + this.adminService.getUsers().subscribe({ + next: (users) => this.users.set(users), + error: () => this.errorMessage.set('Impossible de charger les comptes utilisateurs.'), + }); + } + + onCreateUser(): void { + if (this.createUserForm.invalid) { + this.createUserForm.markAllAsTouched(); + return; + } + const payload = this.createUserForm.getRawValue() as AdminUserCreatePayload; + this.adminService.createUser(payload).subscribe({ + next: () => { + this.successMessage.set('Compte créé avec succès.'); + this.createUserForm.reset({ username: '', email: '', password: '', is_staff: false, is_active: true }); + this.loadUsers(); + }, + error: (err) => this.errorMessage.set(this.readError(err, 'Création du compte impossible.')), + }); + } + + startEditUser(user: AdminUser): void { + this.editingUserId.set(user.id); + this.editUserForm.setValue({ + id: user.id, + username: user.username, + email: user.email, + password: '', + role: (user as any).role ?? 'ADHERENT', + is_staff: user.is_staff, + is_active: user.is_active, + }); + this.setTab('users'); + } + + cancelEditUser(): void { + this.editingUserId.set(null); + this.editUserForm.reset({ id: '', username: '', email: '', password: '', role: 'ADHERENT', is_staff: false, is_active: true }); + } + + onUpdateUser(): void { + if (this.editUserForm.invalid) { + this.editUserForm.markAllAsTouched(); + return; + } + const formValue = this.editUserForm.getRawValue(); + const payload: AdminUserUpdatePayload = { + email: formValue.email, + role: formValue.role, + is_staff: formValue.is_staff, + is_active: formValue.is_active, + }; + if (formValue.username) payload.username = formValue.username; + if (formValue.password) payload.password = formValue.password; + + this.adminService.updateUser(formValue.id, payload).subscribe({ + next: () => { + this.successMessage.set('Compte mis à jour avec succès.'); + this.cancelEditUser(); + this.loadUsers(); + }, + error: (err) => this.errorMessage.set(this.readError(err, 'Mise à jour du compte impossible.')), + }); + } + + onDeleteUser(userId: string): void { + if (!confirm('Supprimer cet utilisateur ?')) return; + this.adminService.deleteUser(userId).subscribe({ + next: () => { + this.successMessage.set('Compte supprimé avec succès.'); + this.loadUsers(); + }, + error: (err) => this.errorMessage.set(this.readError(err, 'Suppression du compte impossible.')), + }); + } + + // Operations + private loadOperations(): void { + this.adminService.getOperations().subscribe({ + next: (ops) => this.operations.set(ops), + error: () => this.errorMessage.set('Impossible de charger les opérations.'), + }); + } + + onCreateOperation(): void { + if (this.createOperationForm.invalid) { + this.createOperationForm.markAllAsTouched(); + return; + } + const formValue = this.createOperationForm.getRawValue(); + const payload: OperationCreatePayload = { + beneficiary: formValue.beneficiary, + amount: formValue.amount, + operation_type: formValue.operation_type as any, + ...(formValue.request ? { request: formValue.request } : {}), + ...(formValue.comment ? { comment: formValue.comment } : {}), + }; + this.adminService.createOperation(payload).subscribe({ + next: () => { + this.successMessage.set('Opération créée avec succès.'); + this.createOperationForm.reset({ beneficiary: '', amount: 0, operation_type: 'CASH', request: '', comment: '' }); + this.loadOperations(); + this.loadUsers(); // refresh credits + }, + error: (err) => this.errorMessage.set(this.readError(err, 'Création de l\'opération impossible.')), + }); + } + + // Filaments + private loadFilaments(): void { + this.adminService.getFilaments().subscribe({ + next: (filaments) => this.filaments.set(filaments), + error: () => this.errorMessage.set('Impossible de charger les filaments.'), + }); + } + + startEditFilament(f: Filament): void { + this.editingFilamentId.set(f.id); + this.filamentForm.setValue({ + color: f.color, + color_name: f.color_name, + type: f.type, + quantity: f.quantity, + price: f.price, + }); + this.setTab('filaments'); + } + + cancelEditFilament(): void { + this.editingFilamentId.set(null); + this.filamentForm.reset({ color: '#ffffff', color_name: '', type: 'PLA', quantity: 0, price: 0 }); + } + + onSubmitFilament(): void { + if (this.filamentForm.invalid) { + this.filamentForm.markAllAsTouched(); + return; + } + const payload = this.filamentForm.getRawValue() as FilamentPayload; + const id = this.editingFilamentId(); + + const obs = id + ? this.adminService.updateFilament(id, payload) + : this.adminService.createFilament(payload); + + obs.subscribe({ + next: () => { + this.successMessage.set(id ? 'Filament mis à jour.' : 'Filament ajouté.'); + this.cancelEditFilament(); + this.loadFilaments(); + }, + error: (err) => this.errorMessage.set(this.readError(err, 'Impossible de sauvegarder le filament.')), + }); + } + + onDeleteFilament(id: number): void { + if (!confirm('Supprimer ce filament ?')) return; + this.adminService.deleteFilament(id).subscribe({ + next: () => { + this.successMessage.set('Filament supprimé.'); + this.loadFilaments(); + }, + error: (err) => this.errorMessage.set(this.readError(err, 'Suppression du filament impossible.')), + }); + } + + // Printers + private loadPrinters(): void { + this.adminService.getPrinters().subscribe({ + next: (printers) => this.printers.set(printers), + error: () => this.errorMessage.set('Impossible de charger les imprimantes.'), + }); + } + + onPrinterStatusChange(printer: Printer, event: Event): void { + const newStatus = (event.target as HTMLSelectElement).value as PrinterStatus; + if (newStatus === printer.status) return; + if (this.updatingPrinterNames.has(printer.name)) return; + + this.updatingPrinterNames.add(printer.name); + this.errorMessage.set(''); + + this.adminService.updatePrinter(printer.name, { status: newStatus }).subscribe({ + next: () => { + this.successMessage.set(`Statut de ${printer.name} mis à jour.`); + this.updatingPrinterNames.delete(printer.name); + this.loadPrinters(); + }, + error: (err) => { + this.errorMessage.set(this.readError(err, 'Impossible de mettre à jour l\'imprimante.')); + this.updatingPrinterNames.delete(printer.name); + this.loadPrinters(); + }, + }); + } + + isUpdatingPrinter(name: string): boolean { + return this.updatingPrinterNames.has(name); + } + + getPrinterBadgeClass(status: PrinterStatus): string { + if (status === 'UP') return 'badge-up'; + if (status === 'USED') return 'badge-used'; + return 'badge-down'; + } + + getUserEmail(userId: string): string { + return this.users().find((u) => u.id === userId)?.email ?? userId.slice(0, 8) + '…'; + } + + getUserRole(userId: string): string { + const user = this.users().find((u) => u.id === userId); + return user && 'role' in user ? (user as any).role : '—'; +} + // Utilities + private readError(error: any, fallback: string): string { + const data = error?.error; + if (typeof data === 'string') return data; + if (data?.detail) return data.detail; + if (data?.non_field_errors?.length) return data.non_field_errors[0]; + if (data?.error) return data.error; + const firstKey = data && typeof data === 'object' ? Object.keys(data)[0] : null; + if (firstKey && Array.isArray(data[firstKey]) && data[firstKey][0]) { + return `${firstKey}: ${data[firstKey][0]}`; + } + return fallback; + } +} \ No newline at end of file diff --git a/prINTech-front/src/app/admin-login/admin-login.css b/prINTech-front/src/app/admin-login/admin-login.css new file mode 100644 index 0000000..892f5e9 --- /dev/null +++ b/prINTech-front/src/app/admin-login/admin-login.css @@ -0,0 +1,95 @@ +.page { + min-height: 100vh; + background: #f0f2f5; + display: flex; + align-items: center; + justify-content: center; +} + +.card { + background: white; + border-radius: 16px; + padding: 40px 36px; + width: 360px; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08); +} + +.logo-area { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 28px; + gap: 10px; +} + +.logo-img { + height: 64px; + width: auto; +} + +.tagline { + font-size: 13px; + color: #8FA3C8; +} + +.error { + background: #fff0f0; + border: 0.5px solid #ffcccc; + color: #c0392b; + border-radius: 8px; + padding: 10px 14px; + font-size: 13px; + margin-bottom: 14px; +} + +.field { + margin-bottom: 14px; +} + +.field label { + display: block; + font-size: 12px; + font-weight: 600; + color: #1B3A6B; + margin-bottom: 6px; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.field input { + width: 100%; + padding: 10px 14px; + border: 0.5px solid rgba(0, 0, 0, 0.15); + border-radius: 8px; + font-size: 14px; + color: #111; + outline: none; + transition: border 0.15s; + font-family: 'Exo 2', sans-serif; +} + +.field input:focus { + border-color: #1B3A6B; +} + +.btn { + width: 100%; + padding: 11px; + margin-top: 8px; + background: #1B3A6B; + color: white; + border: none; + border-radius: 8px; + font-size: 15px; + font-weight: 500; + cursor: pointer; + transition: background 0.15s; + font-family: 'Exo 2', sans-serif; +} + +.btn:hover { background: #142d55; } + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} diff --git a/prINTech-front/src/app/admin-login/admin-login.html b/prINTech-front/src/app/admin-login/admin-login.html new file mode 100644 index 0000000..0378ea6 --- /dev/null +++ b/prINTech-front/src/app/admin-login/admin-login.html @@ -0,0 +1,30 @@ +
+
+
+ PrINTech +
Connexion administrateur
+
+ + @if (errorMessage()) { + + } + +
+
+ + +
+ +
+ + +
+ + +
+
+
diff --git a/prINTech-front/src/app/admin-login/admin-login.ts b/prINTech-front/src/app/admin-login/admin-login.ts new file mode 100644 index 0000000..cc5a49a --- /dev/null +++ b/prINTech-front/src/app/admin-login/admin-login.ts @@ -0,0 +1,63 @@ +import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core'; +import { ReactiveFormsModule, Validators, FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +import { finalize } from 'rxjs'; +import { AuthService } from '../services/auth'; + +@Component({ + selector: 'app-admin-login', + imports: [ReactiveFormsModule], + templateUrl: './admin-login.html', + styleUrl: './admin-login.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AdminLogin { + private readonly auth = inject(AuthService); + private readonly router = inject(Router); + private readonly formBuilder = inject(FormBuilder); + + readonly loading = signal(false); + readonly errorMessage = signal(''); + + readonly loginForm = this.formBuilder.nonNullable.group({ + email: ['', [Validators.required, Validators.email]], + password: ['', [Validators.required, Validators.minLength(8)]], + }); + + onLogin(): void { + if (this.loginForm.invalid || this.loading()) { + this.loginForm.markAllAsTouched(); + return; + } + + this.errorMessage.set(''); + this.loading.set(true); + + const { email, password } = this.loginForm.getRawValue(); + + this.auth + .login(email, password) + .pipe(finalize(() => this.loading.set(false))) + .subscribe({ + next: () => { + this.auth.getCurrentUser().subscribe({ + next: (user) => { + if (!user.is_staff) { + this.auth.logout(); + this.errorMessage.set('Compte non administrateur.'); + return; + } + void this.router.navigate(['/admin/dashboard']); + }, + error: () => { + this.auth.logout(); + this.errorMessage.set('Impossible de vérifier le rôle administrateur.'); + }, + }); + }, + error: () => { + this.errorMessage.set('Identifiants administrateur invalides.'); + }, + }); + } +} diff --git a/prINTech-front/src/app/app.config.ts b/prINTech-front/src/app/app.config.ts new file mode 100644 index 0000000..f0c779e --- /dev/null +++ b/prINTech-front/src/app/app.config.ts @@ -0,0 +1,16 @@ +import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { provideRouter } from '@angular/router'; +import { tokenInterceptor } from './services/token-interceptor'; + +import { routes } from './app.routes'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideBrowserGlobalErrorListeners(), + provideHttpClient( + withInterceptors([tokenInterceptor]) + ), + provideRouter(routes) + ] +}; diff --git a/prINTech-front/src/app/app.routes.ts b/prINTech-front/src/app/app.routes.ts new file mode 100644 index 0000000..0dddfdc --- /dev/null +++ b/prINTech-front/src/app/app.routes.ts @@ -0,0 +1,36 @@ +import { Routes } from '@angular/router'; +import { authGuard } from './guards/auth.guard/auth.guard'; +import { adminGuard } from './guards/admin.guard'; + +export const routes: Routes = [ + { + path: '', + pathMatch: 'full', + loadComponent: () => import('./home/home').then((m) => m.Home), + canActivate: [authGuard], + }, + { + path: 'profile', + loadComponent: () => import('./profile/profile').then((m) => m.Profile), + canActivate: [authGuard], + }, + { + path: 'print', + loadComponent: () => import('./print/print').then((m) => m.Print), + canActivate: [authGuard], + }, + { + path: 'history', + loadComponent: () => import('./history/history').then((m) => m.History), + canActivate: [authGuard], + }, + { + path: 'login', + loadComponent: () => import('./login/login').then((m) => m.Login), + }, + { + path: 'admin/dashboard', + loadComponent: () => import('./admin-dashboard/admin-dashboard').then((m) => m.AdminDashboard), + canActivate: [adminGuard], + }, +]; diff --git a/prINTech-front/src/app/app.spec.ts b/prINTech-front/src/app/app.spec.ts new file mode 100644 index 0000000..7154454 --- /dev/null +++ b/prINTech-front/src/app/app.spec.ts @@ -0,0 +1,27 @@ +import { TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { App } from './app'; + +describe('App', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [App], + providers: [provideRouter([])], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(App); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it('should render app shell', async () => { + const fixture = TestBed.createComponent(App); + await fixture.whenStable(); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('app-header')).not.toBeNull(); + expect(compiled.querySelector('router-outlet')).not.toBeNull(); + }); +}); diff --git a/prINTech-front/src/app/app.ts b/prINTech-front/src/app/app.ts new file mode 100644 index 0000000..5f60d70 --- /dev/null +++ b/prINTech-front/src/app/app.ts @@ -0,0 +1,26 @@ +import { Component, signal } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; +import { Header } from './components/header/header'; +import { provideHttpClient } from '@angular/common/http'; + +@Component({ + selector: 'app-root', + imports: [RouterOutlet, Header ], + template: ` + + +
+ +
+ `, + styles: [ + ` + main { + padding: 16px ; + } + ` + ], +}) +export class App { + protected readonly title = signal('prINTech-frnt'); +} diff --git a/prINTech-front/src/app/components/header/header.css b/prINTech-front/src/app/components/header/header.css new file mode 100644 index 0000000..5d44cbf --- /dev/null +++ b/prINTech-front/src/app/components/header/header.css @@ -0,0 +1,62 @@ +.navbar { + height: 66px; + padding: 0 24px; + display: flex; + align-items: center; + justify-content: space-between; + background: #1B3A6B; + border-bottom: none; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3); +} + +.logo { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +.logo-img { + height: 36px; + width: auto; +} + +.nav-links { + display: flex; + gap: 4px; +} + +.nav-links a { + padding: 6px 14px; + border-radius: 8px; + font-size: 18px; + color: #b1c4e8; + text-decoration: none; + transition: background 0.15s, color 0.15s; +} + +.nav-links a:hover { + background: rgba(255, 255, 255, 0.08); + color: white; +} + +.nav-links a.active { + background: rgba(245, 194, 0, 0.15); + color: #F5C200; + font-weight: 500; +} + +.avatar { + width: 34px; + height: 34px; + border-radius: 50%; + cursor: pointer; + overflow: hidden; + /*border: 2px solid #F5C200;*/ +} + +.avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} \ No newline at end of file diff --git a/prINTech-front/src/app/components/header/header.html b/prINTech-front/src/app/components/header/header.html new file mode 100644 index 0000000..f75dc90 --- /dev/null +++ b/prINTech-front/src/app/components/header/header.html @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/prINTech-front/src/app/components/header/header.spec.ts b/prINTech-front/src/app/components/header/header.spec.ts new file mode 100644 index 0000000..9ef7403 --- /dev/null +++ b/prINTech-front/src/app/components/header/header.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Header } from './header'; + +describe('Header', () => { + let component: Header; + let fixture: ComponentFixture
; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Header], + }).compileComponents(); + + fixture = TestBed.createComponent(Header); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/prINTech-front/src/app/components/header/header.ts b/prINTech-front/src/app/components/header/header.ts new file mode 100644 index 0000000..24afe07 --- /dev/null +++ b/prINTech-front/src/app/components/header/header.ts @@ -0,0 +1,20 @@ +import { Component, inject, signal, computed } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { NgIf } from '@angular/common'; +import { AvatarStateService } from '../../services/avatar-state'; +import { AuthService } from '../../services/auth'; + +@Component({ + selector: 'app-header', + imports: [RouterLink, NgIf], + templateUrl: './header.html', + styleUrl: './header.css', +}) +export class Header { + private readonly avatarState = inject(AvatarStateService); + private readonly auth = inject(AuthService); + + title = signal('prINTech') ; + avatarSrc = this.avatarState.avatarSrc; + isAdmin = computed(() => !!this.auth.currentUser()?.is_staff); +} diff --git a/prINTech-front/src/app/guards/admin.guard.ts b/prINTech-front/src/app/guards/admin.guard.ts new file mode 100644 index 0000000..3599148 --- /dev/null +++ b/prINTech-front/src/app/guards/admin.guard.ts @@ -0,0 +1,25 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { map, catchError, of } from 'rxjs'; +import { AuthService } from '../services/auth'; + +export const adminGuard: CanActivateFn = (route, state) => { + const authService = inject(AuthService); + const router = inject(Router); + + if (!authService.isLoggedIn()) { + return router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } }); + } + + // fast path: if we already have the user loaded, use it + const cached = authService.currentUser(); + if (cached) { + return cached.is_staff ? true : router.createUrlTree(['/']); + } + + // otherwise load the user and decide + return authService.loadCurrentUser().pipe( + map((user) => (user.is_staff ? true : router.createUrlTree(['/']))), + catchError(() => of(router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } }))) + ); +}; diff --git a/prINTech-front/src/app/guards/auth.guard/auth.guard.css b/prINTech-front/src/app/guards/auth.guard/auth.guard.css new file mode 100644 index 0000000..e69de29 diff --git a/prINTech-front/src/app/guards/auth.guard/auth.guard.html b/prINTech-front/src/app/guards/auth.guard/auth.guard.html new file mode 100644 index 0000000..cba5805 --- /dev/null +++ b/prINTech-front/src/app/guards/auth.guard/auth.guard.html @@ -0,0 +1 @@ +

auth.guard works!

diff --git a/prINTech-front/src/app/guards/auth.guard/auth.guard.spec.ts b/prINTech-front/src/app/guards/auth.guard/auth.guard.spec.ts new file mode 100644 index 0000000..24879d8 --- /dev/null +++ b/prINTech-front/src/app/guards/auth.guard/auth.guard.spec.ts @@ -0,0 +1,44 @@ +import { TestBed } from '@angular/core/testing'; +import { provideRouter, Router, UrlTree } from '@angular/router'; +import { AuthService } from '../../services/auth'; +import { authGuard } from './auth.guard'; + +describe('authGuard', () => { + let isLoggedIn = false; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideRouter([]), + { + provide: AuthService, + useValue: { + isLoggedIn: () => isLoggedIn, + }, + }, + ], + }); + }); + + it('returns true when user is logged in', () => { + isLoggedIn = true; + + const result = TestBed.runInInjectionContext(() => + authGuard({} as never, {} as never) + ); + + expect(result).toBe(true); + }); + + it('redirects to /login when user is not logged in', () => { + isLoggedIn = false; + const router = TestBed.inject(Router); + + const result = TestBed.runInInjectionContext(() => + authGuard({} as never, {} as never) + ); + + expect(result instanceof UrlTree).toBe(true); + expect(router.serializeUrl(result as UrlTree)).toBe('/login'); + }); +}); diff --git a/prINTech-front/src/app/guards/auth.guard/auth.guard.ts b/prINTech-front/src/app/guards/auth.guard/auth.guard.ts new file mode 100644 index 0000000..9002643 --- /dev/null +++ b/prINTech-front/src/app/guards/auth.guard/auth.guard.ts @@ -0,0 +1,14 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router' +import { AuthService } from '../../services/auth'; + +export const authGuard: CanActivateFn = (route, state) => { + const authService = inject(AuthService) + const router = inject(Router) + + if (authService.isLoggedIn()) { + return true; + } + + return router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } }); +} diff --git a/prINTech-front/src/app/history/history.css b/prINTech-front/src/app/history/history.css new file mode 100644 index 0000000..f6b1786 --- /dev/null +++ b/prINTech-front/src/app/history/history.css @@ -0,0 +1,146 @@ +.page-content { + max-width: 1100px; + margin: 0 auto; + padding: 24px; +} + +.title { + margin-bottom: 16px; + font-size: clamp(1.4rem, 2vw, 1.9rem); + font-weight: 800; + color: #095c9c; +} + +.feedback { + margin-bottom: 12px; + padding: 10px 12px; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 600; +} + +.feedback.success { + background: #dcfce7; + color: #166534; + border: 1px solid #86efac; +} + +.feedback.error { + background: #fee2e2; + color: #991b1b; + border: 1px solid #fecaca; +} + +.table-wrapper { + overflow-x: auto; + border: 1px solid #e5e7eb; + border-radius: 10px; + background: #fff; +} + +table { + width: 100%; + border-collapse: collapse; + min-width: 900px; +} + +th, +td { + padding: 12px 14px; + border-bottom: 1px solid #eef1f4; + text-align: left; + font-size: 0.95rem; +} + +th { + font-weight: 700; + background: #f8fafc; + color: #334155; +} + +tbody tr:hover { + background: #f9fbff; +} + +.file-name { + max-width: 220px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.status { + display: inline-block; + padding: 4px 10px; + border-radius: 999px; + font-size: 0.8rem; + font-weight: 600; +} + +.status-printed { + background: #dcfce7; + color: #166534; +} + +.status-queued { + background: #dbeafe; + color: #1d4ed8; +} + +.status-failed { + background: #fee2e2; + color: #991b1b; +} + +.reprint-btn { + padding: 6px 10px; + border: 1px solid transparent; + border-radius: 8px; + background: #095c9c; + color: #ffffff; + font-size: 0.85rem; + font-weight: 700; + cursor: pointer; +} + +.reprint-btn:hover { + background: #074b7d; +} + +.reprint-btn:disabled { + opacity: 0.65; + cursor: not-allowed; +} + +.action-buttons { + display: flex; + gap: 8px; + align-items: center; +} + +.status-payment { + background: #fef3c7; + color: #92400e; + border: 1px solid #fde68a; +} + +.pay-btn { + padding: 6px 10px; + border: 1px solid transparent; + border-radius: 8px; + background: #d97706; + color: #ffffff; + font-size: 0.85rem; + font-weight: 700; + cursor: pointer; + transition: background 0.2s; +} + +.pay-btn:hover { + background: #b45309; +} + +.pay-btn:disabled { + opacity: 0.65; + cursor: not-allowed; +} \ No newline at end of file diff --git a/prINTech-front/src/app/history/history.html b/prINTech-front/src/app/history/history.html new file mode 100644 index 0000000..5ad6e85 --- /dev/null +++ b/prINTech-front/src/app/history/history.html @@ -0,0 +1,66 @@ +
+
Historique des impressions
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
IdModèle 3DImprimante 3DMatériauDateStatutAction
+ Aucune impression trouvée pour ce compte. +
{{ item.id | slice:0:8 }}{{ getFileName(item.file?.path) }}{{ item.printer || '-' }}{{ getFilamentName(item.file?.filament) }}{{ item.created_at | date:'short' }} + {{ item.status }} + +
+ + + +
+
+
+
\ No newline at end of file diff --git a/prINTech-front/src/app/history/history.model.ts b/prINTech-front/src/app/history/history.model.ts new file mode 100644 index 0000000..16bdbf4 --- /dev/null +++ b/prINTech-front/src/app/history/history.model.ts @@ -0,0 +1,25 @@ +export type HistoryStatus = + | 'SUBMITTED' + | 'AWAITING_PAYMENT' + | 'PENDING' + | 'PRINTING' + | 'DONE' + | 'FAILED' + | 'CANCELED'; + +export interface HistoryFile { + path: string; + number_of_printing: number; + filament: number; + para_slicer: string | Record; +} + +export interface HistoryItem { + id: string; + user: string; + file: HistoryFile | null; + printer: string | null; + comment: string; + created_at: string; + status: HistoryStatus; +} diff --git a/prINTech-front/src/app/history/history.spec.ts b/prINTech-front/src/app/history/history.spec.ts new file mode 100644 index 0000000..6796296 --- /dev/null +++ b/prINTech-front/src/app/history/history.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { History } from './history'; + +describe('History', () => { + let component: History; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [History], + }).compileComponents(); + + fixture = TestBed.createComponent(History); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/prINTech-front/src/app/history/history.ts b/prINTech-front/src/app/history/history.ts new file mode 100644 index 0000000..4b19d7e --- /dev/null +++ b/prINTech-front/src/app/history/history.ts @@ -0,0 +1,142 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnInit, inject, ChangeDetectorRef } from '@angular/core'; +import { HistoryServices, Filament } from '../services/history-services'; +import { HistoryItem } from './history.model'; + +@Component({ + selector: 'app-history', + imports: [CommonModule], + templateUrl: './history.html', + styleUrl: './history.css', +}) +export class History implements OnInit{ + private readonly historyService = inject(HistoryServices); + private readonly cdr = inject(ChangeDetectorRef); + + fullHistory: HistoryItem[] = []; + filteredHistory: HistoryItem[] = []; + filaments: Filament[] = []; + SearchQuery = ''; + + errorMessage = ''; + successMessage = ''; + relaunchingIds = new Set(); + payingIds = new Set(); // Track loading state for payments + + ngOnInit(): void { + this.loadFilaments(); + this.loadHistory(); + } + + onSearch(event: Event) { + this.SearchQuery = (event.target as HTMLInputElement).value; + this.applyFilters(); + } + + private applyFilters(): void { + const query = this.SearchQuery.toLowerCase(); + if (!query) { + this.filteredHistory = this.fullHistory; + } else { + this.filteredHistory = this.fullHistory.filter(item => { + const fileName = this.getFileName(item.file?.path).toLowerCase(); + return fileName.includes(query); + }); + } + this.cdr.detectChanges(); + } + + private loadFilaments(): void { + this.historyService.getFilaments().subscribe({ + next: (filaments) => { + this.filaments = filaments; + this.cdr.detectChanges(); + }, + error: () => { + this.filaments = []; + }, + }); + } + + private loadHistory(): void { + this.historyService.getHistory().subscribe({ + next: (response: any) => { + const items = response?.results ? response.results : (Array.isArray(response) ? response : []); + this.fullHistory = items; + if (items.length === 0 && !Array.isArray(response)) { + this.errorMessage = 'Parsed items is empty but response was ' + JSON.stringify(response); + } + this.applyFilters(); + }, + error: (err) => { + this.errorMessage = err.message || 'Error occurred'; + this.fullHistory = []; + this.applyFilters(); + }, + }); + } + + getFilamentName(filamentId: number | null | undefined): string { + if (filamentId == null) return '-'; + const filament = this.filaments.find(f => f.id === filamentId); + if (!filament) return `ID: ${filamentId}`; + return `${filament.type} (${filament.color_name})`; + } + + getFileName(path: string | null | undefined): string { + if (!path) return '-'; + return path.split('/').pop() || path; + } + + onRelaunch(item: HistoryItem): void { + if (this.relaunchingIds.has(item.id) || this.payingIds.has(item.id)) return; + + this.errorMessage = ''; + this.successMessage = ''; + this.relaunchingIds.add(item.id); + + this.historyService.relaunchRequest(item.id).subscribe({ + next: () => { + this.successMessage = 'Demande relancée avec succès.'; + this.relaunchingIds.delete(item.id); + this.loadHistory(); + this.cdr.detectChanges(); + }, + error: (err) => { + this.errorMessage = err?.error?.error || 'Impossible de relancer cette impression.'; + this.relaunchingIds.delete(item.id); + this.cdr.detectChanges(); + }, + }); + } + + onPay(item: HistoryItem): void { + if (this.payingIds.has(item.id) || this.relaunchingIds.has(item.id)) return; + + this.errorMessage = ''; + this.successMessage = ''; + this.payingIds.add(item.id); + + this.historyService.payRequest(item.id).subscribe({ + next: () => { + this.successMessage = 'Paiement effectué avec succès !'; + this.payingIds.delete(item.id); + this.loadHistory(); + this.cdr.detectChanges(); + }, + error: (err) => { + this.errorMessage = err?.error?.error || 'Échec du paiement. Vérifiez votre solde.'; + this.payingIds.delete(item.id); + this.cdr.detectChanges(); + }, + }); + } + + isRelaunching(id: string): boolean { + return this.relaunchingIds.has(id); + } + + isPaying(id: string): boolean { + return this.payingIds.has(id); + } +} \ No newline at end of file diff --git a/prINTech-front/src/app/home/components/metric-card/metric-card.css b/prINTech-front/src/app/home/components/metric-card/metric-card.css new file mode 100644 index 0000000..49216e4 --- /dev/null +++ b/prINTech-front/src/app/home/components/metric-card/metric-card.css @@ -0,0 +1,73 @@ +.cards-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; +} + +.metric-card { + border-radius: 14px; + padding: 18px 16px; + background: #ffffff; + border: 1px solid #d8deea; + box-shadow: 0 8px 24px rgba(9, 92, 156, 0.08); + display: grid; + gap: 8px; + min-height: 150px; +} + +.label { + font-size: 0.84rem; + font-weight: 700; + letter-spacing: 0.02em; + color: #5f6f92; + text-transform: uppercase; +} + +.value { + font-size: clamp(1.2rem, 2vw, 1.7rem); + font-weight: 800; + color: #111111; + line-height: 1.2; +} + +.sub { + font-size: 0.9rem; + color: #475569; +} + +.badge { + display: inline-flex; + align-items: center; + border-radius: 999px; + padding: 4px 10px; + font-size: 0.78rem; + font-weight: 700; +} + +.badge-success { + background: #eaf7ef; + color: #136a39; + border: 1px solid #b7e1c5; +} + +.badge-danger { + background: #fdecec; + color: #9f1239; + border: 1px solid #f5c2c7; +} + +@media (max-width: 960px) { + .cards-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 640px) { + .cards-grid { + grid-template-columns: 1fr; + } + + .metric-card { + min-height: 130px; + } +} \ No newline at end of file diff --git a/prINTech-front/src/app/home/components/metric-card/metric-card.html b/prINTech-front/src/app/home/components/metric-card/metric-card.html new file mode 100644 index 0000000..95b2129 --- /dev/null +++ b/prINTech-front/src/app/home/components/metric-card/metric-card.html @@ -0,0 +1,22 @@ +
+
+
Crédit restant
+
{{UserCredit()}}€
+
Adhérent {{ is_active() ? 'actif' : 'inactif' }}
+
+
+
Imprimantes actives
+
{{Active_printers()}} / {{Total_printers()}}
+
+ {{Printer_statuses()}} +
+
+
+
Travaux en file
+
{{ Queue_size() }}
+
+
+
\ No newline at end of file diff --git a/prINTech-front/src/app/home/components/metric-card/metric-card.ts b/prINTech-front/src/app/home/components/metric-card/metric-card.ts new file mode 100644 index 0000000..cadc670 --- /dev/null +++ b/prINTech-front/src/app/home/components/metric-card/metric-card.ts @@ -0,0 +1,25 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { HomeService } from '../../../services/home'; + +@Component({ + selector: 'app-metric-card', + imports: [], + templateUrl: './metric-card.html', + styleUrl: './metric-card.css', +}) +export class MetricCard implements OnInit { + private readonly homeService = inject(HomeService) + ngOnInit(): void { + this.homeService.loadUserInfo(); + this.homeService.getActivePrinters(); + this.homeService.GetQueue(); + } + + UserCredit = this.homeService.userCredit + Active_printers = this.homeService.active_printers + Total_printers = this.homeService.total_printers + Printer_statuses = this.homeService.printers_status + Queue_size = this.homeService.queue_size + is_active = this.homeService.is_active + +} diff --git a/prINTech-front/src/app/home/home.css b/prINTech-front/src/app/home/home.css new file mode 100644 index 0000000..a4be739 --- /dev/null +++ b/prINTech-front/src/app/home/home.css @@ -0,0 +1,186 @@ +.page-content { + max-width: 1100px; + margin: 0 auto; + padding: 24px 12px 32px; + display: grid; + gap: 12px; +} + +.greeting { + font-size: clamp(1.6rem, 2.5vw, 2.2rem); + font-weight: 800; + line-height: 1.2; + color: #095c9c; +} + +.subgreeting { + margin: 0; + color: #4b5563; + font-size: 1rem; +} + +.cta-btn { + justify-self: start; + margin-top: 10px; + margin-bottom: 8px; + padding: 11px 18px; + border: 2px solid transparent; + border-radius: 999px; + background: #095c9c; + color: #ffffff; + font-size: 0.95rem; + font-weight: 700; + cursor: pointer; + transition: transform 0.15s ease, background-color 0.2s ease, border-color 0.2s ease; +} + +.cta-btn:hover { + background: #074b7d; + transform: translateY(-1px); +} + +.cta-btn:focus-visible { + outline: none; + border-color: #f8cc08; +} + +.empty-state { + margin-top: 8px; + border: 1px dashed #98a7cb; + border-radius: 12px; + background: #f8fbff; + color: #1f2937; + padding: 14px 16px; +} + +.link { + color: #095c9c; + font-weight: 700; + cursor: pointer; +} + +.link:hover { + text-decoration: underline; +} + +.printers-section { + margin-top: 18px; + padding-top: 10px; + display: grid; + gap: 14px; +} + +.section-head h2 { + margin: 0; + font-size: clamp(1.3rem, 2.2vw, 1.8rem); + color: #095c9c; + font-weight: 800; +} + +.section-head p { + margin: 6px 0 0; + color: #4b5563; +} + +.printers-grid { + display: grid; + grid-template-columns: 1fr; + gap: 20px; +} + +.printer-card { + background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%); + border: 1px solid #dfe6f2; + border-radius: 14px; + box-shadow: 0 12px 28px rgba(9, 92, 156, 0.1); + overflow: hidden; + display: grid; + grid-template-columns: minmax(320px, 36%) minmax(0, 1fr); + grid-template-areas: 'media content'; + min-height: 320px; +} + +.printer-card:nth-child(even) { + grid-template-columns: minmax(0, 1fr) minmax(320px, 36%); + grid-template-areas: 'content media'; +} + +.printer-image { + grid-area: media; + width: 100%; + height: 100%; + object-fit: contain; + background: #f6f9ff; + padding: 14px; + border: none; + border-radius: 0; + box-sizing: border-box; +} + +.printer-content { + grid-area: content; + padding: 24px 28px 22px; + display: grid; + gap: 10px; + align-content: center; + min-width: 0; + border-left: 1px solid #e5eaf3; +} + +.printer-card:nth-child(even) .printer-content { + border-left: none; + border-right: 1px solid #e5eaf3; +} + +.printer-content h3 { + margin: 0; + font-size: clamp(1.2rem, 2vw, wait now that im looking at it theres a 1.55rem); + color: #111111; + font-weight: 800; +} + +.printer-content p { + margin: 0; + color: #475569; + line-height: 1.55; + font-size: 1rem; +} + +.printer-content ul { + margin: 6px 0 0; + padding-left: 20px; + color: #1f2937; + display: grid; + gap: 6px; +} + +@media (max-width: 960px) { + .printers-grid { + grid-template-columns: 1fr; + } + + .printer-card, + .printer-card:nth-child(even) { + grid-template-columns: 1fr; + grid-template-areas: + 'media' + 'content'; + min-height: auto; + } + + .printer-image { + height: 260px; + } + + .printer-content { + padding: 18px 18px 20px; + border-left: none; + border-right: none; + } +} + +@media (max-width: 640px) { + .page-content { + padding-inline: 4px; + } +} diff --git a/prINTech-front/src/app/home/home.html b/prINTech-front/src/app/home/home.html new file mode 100644 index 0000000..1cc1ccf --- /dev/null +++ b/prINTech-front/src/app/home/home.html @@ -0,0 +1,88 @@ +
+
Bonjour, {{Username()}}
+

Voici l'état de vos impressions et du parc d'imprimantes.

+ + + + + +
+
+

Nos imprimantes

+

Trois machines complémentaires pour des impressions rapides, propres et fiables.

+
+ +
+
+ Imprimante 3D Creality K1C +
+

Creality K1C

+

+ Imprimante FDM rapide idéale pour le prototypage quotidien. Elle offre une bonne + qualité de surface et des temps d'impression courts pour les pièces fonctionnelles. +

+
    +
  • Haute vitesse d'impression
  • +
  • Très bon choix pour PLA, PETG et TPU
  • +
  • Parfaite pour les itérations rapides
  • +
+
+
+ +
+ Imprimante 3D Snapmaker U1 +
+

Snapmaker U1

+

+ Machine polyvalente orientée précision et stabilité. Adaptée aux impressions plus + techniques, avec un excellent rendu sur les pièces détaillées. +

+
    +
  • Grande précision sur les petits détails
  • +
  • Très bon comportement sur pièces techniques
  • +
  • Bonne régularité pour les impressions longues
  • +
+
+
+ +
+ Imprimante 3D Prusa i3 MK3 +
+

Prusa i3 MK3

+

+ Imprimante reconnue pour sa fiabilité et sa qualité d'impression constante. Idéale pour + des pièces précises, des longues sessions d'impression et un excellent rendu global. +

+
    +
  • Fiabilité élevée sur les impressions longues
  • +
  • Très bonne précision dimensionnelle
  • +
  • Qualité stable pour prototypes et pièces finales
  • +
+
+
+
+
+
+ + + diff --git a/prINTech-front/src/app/home/home.spec.ts b/prINTech-front/src/app/home/home.spec.ts new file mode 100644 index 0000000..61ab99d --- /dev/null +++ b/prINTech-front/src/app/home/home.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Home } from './home'; + +describe('Home', () => { + let component: Home; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Home], + }).compileComponents(); + + fixture = TestBed.createComponent(Home); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/prINTech-front/src/app/home/home.ts b/prINTech-front/src/app/home/home.ts new file mode 100644 index 0000000..4382f90 --- /dev/null +++ b/prINTech-front/src/app/home/home.ts @@ -0,0 +1,22 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { NgOptimizedImage } from '@angular/common'; +import { RouterLink } from '@angular/router'; +import { HomeService } from '../services/home'; +import { MetricCard } from './components/metric-card/metric-card'; + +@Component({ + selector: 'app-home', + imports: [RouterLink, NgOptimizedImage, MetricCard], + templateUrl: './home.html', + styleUrl: './home.css', +}) +export class Home implements OnInit{ + private readonly homeService = inject(HomeService); + + ngOnInit(): void { + this.homeService.loadUserInfo(); + this.homeService.getActivePrinters(); + } + + Username = this.homeService.username +} diff --git a/prINTech-front/src/app/login/login.css b/prINTech-front/src/app/login/login.css new file mode 100644 index 0000000..069213f --- /dev/null +++ b/prINTech-front/src/app/login/login.css @@ -0,0 +1,95 @@ +.page { + min-height: 100vh; + background: #f0f2f5; + display: flex; + align-items: center; + justify-content: center; +} + +.card { + background: white; + border-radius: 16px; + padding: 40px 36px; + width: 360px; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08); +} + +.logo-area { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 28px; + gap: 10px; +} + +.logo-img { + height: 64px; + width: auto; +} + +.tagline { + font-size: 13px; + color: #8FA3C8; +} + +.error { + background: #fff0f0; + border: 0.5px solid #ffcccc; + color: #c0392b; + border-radius: 8px; + padding: 10px 14px; + font-size: 13px; + margin-bottom: 14px; +} + +.field { + margin-bottom: 14px; +} + +.field label { + display: block; + font-size: 12px; + font-weight: 600; + color: #1B3A6B; + margin-bottom: 6px; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.field input { + width: 100%; + padding: 10px 14px; + border: 0.5px solid rgba(0, 0, 0, 0.15); + border-radius: 8px; + font-size: 14px; + color: #111; + outline: none; + transition: border 0.15s; + font-family: 'Exo 2', sans-serif; +} + +.field input:focus { + border-color: #1B3A6B; +} + +.btn { + width: 100%; + padding: 11px; + margin-top: 8px; + background: #1B3A6B; + color: white; + border: none; + border-radius: 8px; + font-size: 15px; + font-weight: 500; + cursor: pointer; + transition: background 0.15s; + font-family: 'Exo 2', sans-serif; +} + +.btn:hover { background: #142d55; } + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} \ No newline at end of file diff --git a/prINTech-front/src/app/login/login.html b/prINTech-front/src/app/login/login.html new file mode 100644 index 0000000..44a5bb9 --- /dev/null +++ b/prINTech-front/src/app/login/login.html @@ -0,0 +1,32 @@ +
+
+ +
+ PrINTech +
Portail d'impression 3Ds
+
+ + @if (errorMessage()) { + + } + +
+
+ + +
+ +
+ + +
+ + +
+ +
+
\ No newline at end of file diff --git a/prINTech-front/src/app/login/login.spec.ts b/prINTech-front/src/app/login/login.spec.ts new file mode 100644 index 0000000..1ee4152 --- /dev/null +++ b/prINTech-front/src/app/login/login.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Login } from './login'; + +describe('Login', () => { + let component: Login; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Login], + }).compileComponents(); + + fixture = TestBed.createComponent(Login); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/prINTech-front/src/app/login/login.ts b/prINTech-front/src/app/login/login.ts new file mode 100644 index 0000000..c53c26c --- /dev/null +++ b/prINTech-front/src/app/login/login.ts @@ -0,0 +1,62 @@ +import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core'; +import { ReactiveFormsModule, Validators, FormBuilder } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { finalize, switchMap } from 'rxjs'; +import { AuthService } from '../services/auth'; + +@Component({ + selector: 'app-login', + imports: [ReactiveFormsModule], + templateUrl: './login.html', + styleUrl: './login.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class Login { + private readonly auth = inject(AuthService); + private readonly router = inject(Router); + private readonly route = inject(ActivatedRoute); + private readonly formBuilder = inject(FormBuilder); + + readonly loading = signal(false); + readonly errorMessage = signal(''); + + readonly loginForm = this.formBuilder.nonNullable.group({ + email: ['', [Validators.required, Validators.email]], + password: ['', [Validators.required, Validators.minLength(8)]], + }); + + onLogin(): void { + if (this.loginForm.invalid || this.loading()) { + this.loginForm.markAllAsTouched(); + return; + } + + this.errorMessage.set(''); + this.loading.set(true); + + const { email, password } = this.loginForm.getRawValue(); + + this.auth + .login(email, password) + .pipe(switchMap(() => this.auth.loadCurrentUser())) + .pipe(finalize(() => this.loading.set(false))) + .subscribe({ + next: (user) => { + const returnUrl = this.route.snapshot.queryParamMap.get('returnUrl'); + if (returnUrl && returnUrl.startsWith('/')) { + void this.router.navigateByUrl(returnUrl); + return; + } + + if (user.is_staff) { + void this.router.navigate(['/admin/dashboard']); + } else { + void this.router.navigate(['/']); + } + }, + error: () => { + this.errorMessage.set('Adresse e-mail ou mot de passe invalide.'); + }, + }); + } +} diff --git a/prINTech-front/src/app/print/print.css b/prINTech-front/src/app/print/print.css new file mode 100644 index 0000000..ad0740b --- /dev/null +++ b/prINTech-front/src/app/print/print.css @@ -0,0 +1,298 @@ +.print-page { + max-width: 1100px; + margin: 0 auto; + padding: 24px 12px 36px; + display: grid; + gap: 18px; +} + +.print-header h1 { + margin: 0; + font-size: clamp(1.6rem, 2.5vw, 2.2rem); + color: #095c9c; +} + +.print-header p { + margin: 8px 0 0; + color: #4b5563; +} + +.print-grid { + display: grid; + grid-template-columns: 2fr 2fr 1.4fr; + gap: 14px; +} + +.panel { + background: #ffffff; + border: 1px solid #dfe6f2; + border-radius: 14px; + padding: 16px; + box-shadow: 0 10px 24px rgba(9, 92, 156, 0.08); + display: grid; + gap: 12px; + align-content: start; +} + +.panel h2 { + margin: 0; + font-size: 1.08rem; + color: #111827; +} + +.upload-box { + border: 1px dashed #98a7cb; + border-radius: 12px; + background: #f8fbff; + padding: 20px; + text-align: center; + display: grid; + gap: 8px; +} + +.upload-title { + margin: 0; + font-weight: 700; + color: #1f2937; +} + +.upload-subtitle { + margin: 0; + color: #6b7280; + font-size: 0.92rem; +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.form-block { + display: grid; + gap: 6px; +} + +.form-block label { + font-size: 0.88rem; + font-weight: 600; + color: #374151; +} + +.form-block input, +.form-block select, +.form-block textarea { + border: 1px solid #d1d9e6; + border-radius: 10px; + padding: 10px 11px; + font-size: 0.95rem; + color: #374151; + background: #f9fbff; +} + +.summary ul { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 8px; +} + +.summary li { + display: flex; + justify-content: space-between; + gap: 8px; + border-bottom: 1px solid #edf1f8; + padding-bottom: 8px; +} + +.summary span { + color: #6b7280; +} + +.summary strong { + color: #111827; +} + +.summary-note { + margin: 0; + font-size: 0.88rem; + color: #6b7280; +} + +.message { + padding: 14px 18px; + border-radius: 12px; + font-weight: 700; + display: flex; + align-items: center; + gap: 12px; + justify-content: center; + margin: 18px 0; +} + +.message--success { + background: linear-gradient(90deg, rgba(16,185,129,0.10), rgba(16,185,129,0.03)); + color: #064e3b; + border: 1px solid rgba(16,185,129,0.18); + box-shadow: 0 12px 30px rgba(16,185,129,0.06); + font-size: 1.02rem; + width: 100%; + box-sizing: border-box; +} + +.message--error { + background: linear-gradient(90deg, rgba(239,68,68,0.08), rgba(239,68,68,0.02)); + color: #7f1d1d; + border: 1px solid rgba(239,68,68,0.14); + box-shadow: 0 12px 30px rgba(239,68,68,0.04); + font-size: 1.02rem; + width: 100%; + box-sizing: border-box; +} + +.message--success::before, +.message--error::before { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 50%; + font-size: 1.05rem; +} + +.message--success::before { + content: '✔'; + background: rgba(16,185,129,0.12); + color: #064e3b; +} + +.message--error::before { + content: '⚠'; + background: rgba(239,68,68,0.10); + color: #7f1d1d; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeOut { + to { + opacity: 0; + } +} + +.top-banner { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + z-index: 9999; + width: 280px; + padding: 20px; + border-radius: 12px; + display: flex; + align-items: center; + gap: 14px; + font-weight: 600; + font-size: 1.05rem; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); + animation: slideDown 0.3s ease-out, fadeOut 0.5s ease-in 5.5s forwards; + background: white; + border-left: 5px solid; +} + +.top-banner::before { + content: ''; + display: inline-flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 50%; + font-size: 1.2rem; + flex-shrink: 0; +} + +.top-banner--success { + border-left-color: #10b981; + color: #0f67a6; +} + +.top-banner--success::before { + content: '✓'; + background: #ecfdf5; + color: #10b981; + font-weight: 900; +} + +.top-banner--error { + border-left-color: #ef4444; + color: #7f1d1d; +} + +.top-banner--error::before { + content: '✕'; + background: #fef2f2; + color: #ef4444; + font-weight: 900; +} + +.actions { + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.ghost-btn, +.primary-btn { + border-radius: 999px; + padding: 10px 16px; + font-weight: 700; + cursor: pointer; + border: 2px solid transparent; +} + +.ghost-btn { + background: #ffffff; + border-color: #cbd5e1; + color: #1f2937; +} + +.primary-btn { + background: #095c9c; + color: #ffffff; +} + +@media (max-width: 960px) { + .print-grid { + grid-template-columns: 1fr; + } + + .actions { + justify-content: stretch; + } + + .actions button { + flex: 1; + } +} + +@media (max-width: 640px) { + .print-page { + padding-inline: 6px; + } + + .form-row { + grid-template-columns: 1fr; + } +} diff --git a/prINTech-front/src/app/print/print.html b/prINTech-front/src/app/print/print.html new file mode 100644 index 0000000..74d193f --- /dev/null +++ b/prINTech-front/src/app/print/print.html @@ -0,0 +1,99 @@ +
+ {{ successMessage() }} +
+ + + \ No newline at end of file diff --git a/prINTech-front/src/app/print/print.spec.ts b/prINTech-front/src/app/print/print.spec.ts new file mode 100644 index 0000000..00083d2 --- /dev/null +++ b/prINTech-front/src/app/print/print.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Print } from './print'; + +describe('Print', () => { + let component: Print; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Print], + }).compileComponents(); + + fixture = TestBed.createComponent(Print); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/prINTech-front/src/app/print/print.ts b/prINTech-front/src/app/print/print.ts new file mode 100644 index 0000000..7eb4cbd --- /dev/null +++ b/prINTech-front/src/app/print/print.ts @@ -0,0 +1,149 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject, OnInit, signal, computed } from '@angular/core'; +import { finalize } from 'rxjs'; +import { Print as printService, Filament, PrintRequestResponse } from '../services/print'; + +@Component({ + selector: 'app-print', + imports: [CommonModule], + templateUrl: './print.html', + styleUrl: './print.css', +}) +export class Print implements OnInit { + private readonly printService = inject(printService); + + readonly loading = signal(false); + readonly errorMessage = signal(''); + readonly successMessage = signal(''); + readonly filaments = signal([]); + + readonly selectedMaterial = signal(''); + readonly selectedColor = signal(''); + readonly selectedQuantity = signal(1); + + readonly isFileSelected = signal(false); + private selectedFile: File | null = null; + + readonly availableMaterials = computed(() => { + return Array.from(new Set(this.filaments().map((f) => f.type))); + }); + + readonly availableColors = computed(() => { + const material = this.selectedMaterial(); + if (!material) return []; + return Array.from(new Set( + this.filaments() + .filter((f) => f.type === material) + .map((f) => f.color_name) + )); + }); + + readonly selectedFilament = computed(() => { + const material = this.selectedMaterial(); + const color = this.selectedColor(); + return this.filaments().find(f => f.type === material && f.color_name === color) ?? null; + }); + + readonly selectedFilamentId = computed(() => { + return this.selectedFilament()?.id ?? null; + }); + ngOnInit(): void { + this.loadFilaments(); + } + + onMaterialChange(event: Event): void { + const target = event.target as HTMLSelectElement; + this.selectedMaterial.set(target.value); + + // Automatically match the first available color option for this new material + const colors = this.availableColors(); + this.selectedColor.set(colors.length > 0 ? colors[0] : ''); + } + + onColorChange(event: Event): void { + const target = event.target as HTMLSelectElement; + this.selectedColor.set(target.value); + } + + onQuantityChange(event: Event): void { + const value = Number((event.target as HTMLInputElement).value); + this.selectedQuantity.set(Math.max(1, value || 1)); + } + + onFileSelected(event: Event): void { + const input = event.target as HTMLInputElement; + const file = input.files?.[0] ?? null; + this.selectedFile = file; + this.isFileSelected.set(file !== null); + } + + SendRequest(fileInput: HTMLInputElement, comment: string): void { + if (this.loading()) return; + + const file = fileInput.files?.[0] ?? this.selectedFile; + const filamentId = this.selectedFilamentId(); + + this.errorMessage.set(''); + this.successMessage.set(''); + + if (!file) { + this.errorMessage.set('Veuillez sélectionner un fichier STL.'); + return; + } + + if (filamentId === null) { + this.errorMessage.set('Aucun filament correspondant au matériau et à la couleur sélectionnés.'); + return; + } + + this.loading.set(true); + + this.printService + .SendRequest({ + filament: filamentId, + comment: comment.trim(), + path: file, + number_of_printing: this.selectedQuantity(), + }) + .pipe(finalize(() => this.loading.set(false))) + .subscribe({ + next: (response: PrintRequestResponse) => { + this.successMessage.set(`Demande envoyée avec succès. Statut: ${response.status}.`); + this.resetForm(fileInput); + }, + error: () => { + this.errorMessage.set("Impossible d'envoyer la demande. Vérifiez le backend et les champs saisis."); + this.scrollToTop(); + setTimeout(() => this.errorMessage.set(''), 8000); + }, + }); + } + + private loadFilaments(): void { + this.printService.getFilaments().subscribe({ + next: (filaments) => { + this.filaments.set(filaments); + if (filaments.length > 0) { + this.selectedMaterial.set(filaments[0].type); + this.selectedColor.set(filaments[0].color_name); + } + }, + error: () => { + this.errorMessage.set('Impossible de charger les filaments depuis le backend.'); + }, + }); + } + + private resetForm(fileInput: HTMLInputElement): void { + this.scrollToTop(); + fileInput.value = ''; + this.selectedFile = null; + this.isFileSelected.set(false); + this.selectedQuantity.set(1); + setTimeout(() => this.successMessage.set(''), 6000); + } + + private scrollToTop(): void { + try { window.scrollTo({ top: 0, behavior: 'smooth' }); } catch {} + } +} \ No newline at end of file diff --git a/prINTech-front/src/app/profile/change-password/change-password.css b/prINTech-front/src/app/profile/change-password/change-password.css new file mode 100644 index 0000000..f439320 --- /dev/null +++ b/prINTech-front/src/app/profile/change-password/change-password.css @@ -0,0 +1,99 @@ +.profile-card { + background: #ffffff; + border: 1px solid #d8deea; + border-radius: 14px; + padding: 18px 20px; + box-shadow: 0 8px 24px rgba(9, 92, 156, 0.08); + display: grid; + gap: 12px; +} + +.profile-card h2 { + margin: 0; + font-size: 1.05rem; + font-weight: 800; + color: #111827; +} + +.form-grid { + display: grid; + gap: 12px; +} + +.field { + display: grid; + gap: 6px; +} + +.field span { + font-size: 0.84rem; + font-weight: 700; + letter-spacing: 0.02em; + color: #5f6f92; + text-transform: uppercase; +} + +.field input { + width: 100%; + padding: 10px 12px; + border: 1px solid #d8deea; + border-radius: 10px; + font-size: 0.95rem; + color: #1f2937; + background: #ffffff; + box-sizing: border-box; +} + +.field input:focus { + outline: none; + border-color: #095c9c; +} + +.action-btn { + padding: 10px 16px; + border: 2px solid transparent; + border-radius: 999px; + background: #095c9c; + color: #ffffff; + text-decoration: none; + font-size: 0.9rem; + font-weight: 700; + cursor: pointer; + transition: transform 0.15s ease, background-color 0.2s ease, border-color 0.2s ease; +} + +.action-btn:hover { + background: #074b7d; + text-decoration: none; + transform: translateY(-1px); +} + +.action-btn:focus-visible { + outline: none; + border-color: #f8cc08; +} + +.action-btn:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none; +} + +.message { + padding: 10px 12px; + border-radius: 10px; + font-size: 0.9rem; + font-weight: 600; +} + +.message.error { + background: #fdecec; + color: #9f1239; + border: 1px solid #f5c2c7; +} + +.message.success { + background: #eaf7ef; + color: #136a39; + border: 1px solid #b7e1c5; +} diff --git a/prINTech-front/src/app/profile/change-password/change-password.html b/prINTech-front/src/app/profile/change-password/change-password.html new file mode 100644 index 0000000..c6845a2 --- /dev/null +++ b/prINTech-front/src/app/profile/change-password/change-password.html @@ -0,0 +1,38 @@ +
+

Changer le mot de passe

+ + @if (errorMessage()) { + + } + + @if (successMessage()) { +
+ {{ successMessage() }} +
+ } + +
+ + + + + @if (changePasswordForm.hasError('passwordMismatch') && changePasswordForm.touched) { + + } + + +
+
diff --git a/prINTech-front/src/app/profile/change-password/change-password.spec.ts b/prINTech-front/src/app/profile/change-password/change-password.spec.ts new file mode 100644 index 0000000..eb7e1a9 --- /dev/null +++ b/prINTech-front/src/app/profile/change-password/change-password.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChangePassword } from './change-password'; + +describe('ChangePassword', () => { + let component: ChangePassword; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ChangePassword], + }).compileComponents(); + + fixture = TestBed.createComponent(ChangePassword); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/prINTech-front/src/app/profile/change-password/change-password.ts b/prINTech-front/src/app/profile/change-password/change-password.ts new file mode 100644 index 0000000..58e7cc9 --- /dev/null +++ b/prINTech-front/src/app/profile/change-password/change-password.ts @@ -0,0 +1,65 @@ +import { Component, inject, signal } from '@angular/core'; +import { AuthService } from '../../services/auth'; +import { ReactiveFormsModule, Validators, FormBuilder, AbstractControl, ValidationErrors } from '@angular/forms'; +import { finalize } from 'rxjs'; + + +@Component({ + selector: 'app-change-password', + imports: [ReactiveFormsModule], + templateUrl: './change-password.html', + styleUrl: './change-password.css', +}) +export class ChangePassword { + private readonly authService = inject(AuthService) + private readonly formBuilder = inject(FormBuilder) + + readonly loading = signal(false); + readonly errorMessage = signal(''); + readonly successMessage = signal(''); + + readonly changePasswordForm = this.formBuilder.nonNullable.group( + { + old_password: ['', [Validators.required, Validators.minLength(8)]], + new_password: ['', [Validators.required, Validators.minLength(8)]], + confirm_password: ['', [Validators.required, Validators.minLength(8)]] + }, + { validators: [this.passwordsMatchValidator] } + ) + + private passwordsMatchValidator(control: AbstractControl): ValidationErrors | null { + const newPassword = control.get('new_password')?.value + const confirmPassword = control.get('confirm_password')?.value + return newPassword === confirmPassword ? null : { passwordMismatch: true } + } + + onChangePassword(): void { + if (this.changePasswordForm.invalid || this.loading()) { + this.changePasswordForm.markAllAsTouched(); + return; + } + + this.errorMessage.set(''); + this.successMessage.set(''); + this.loading.set(true); + + const { old_password, new_password, confirm_password } = this.changePasswordForm.getRawValue(); + + this.authService + .change_password(old_password, new_password, confirm_password) + .pipe(finalize(() => this.loading.set(false))) + .subscribe({ + next: () => { + this.successMessage.set('Mot de passe mis à jour avec succès.'); + this.changePasswordForm.reset({ + old_password: '', + new_password: '', + confirm_password: '', + }); + }, + error: () => { + this.errorMessage.set('Impossible de mettre à jour le mot de passe. Vérifiez vos informations.'); + }, + }); + } +} diff --git a/prINTech-front/src/app/profile/profile-picture/profile-picture.css b/prINTech-front/src/app/profile/profile-picture/profile-picture.css new file mode 100644 index 0000000..8d56655 --- /dev/null +++ b/prINTech-front/src/app/profile/profile-picture/profile-picture.css @@ -0,0 +1,115 @@ +.profile-picture-widget { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + width: 100%; +} + +.avatar-wrapper { + position: relative; + width: 120px; + height: 120px; + flex: 0 0 120px; +} + +.avatar-pic { + width: 70%; + height: 70%; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border-radius: 50%; + object-fit: cover; + background: #f8fbff; +} + +.avatar-frame { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + object-fit: contain; +} + +.file-name { + margin: 0; + color: #475569; + font-size: 0.95rem; +} + +.file-upload { + display: grid; + gap: 10px; + justify-items: start; + flex: 1; +} + +.actions { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.file-input { + display: none; +} + +.upload-btn { + justify-self: start; + padding: 10px 16px; + border: 2px solid transparent; + border-radius: 999px; + background: #095c9c; + color: #ffffff; + font-size: 0.9rem; + font-weight: 700; + cursor: pointer; + transition: transform 0.15s ease, background-color 0.2s ease, border-color 0.2s ease; +} + +.upload-btn:hover { + background: #074b7d; + transform: translateY(-1px); +} + +.upload-btn:focus-visible { + outline: none; + border-color: #f8cc08; +} + +.upload-btn:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none; +} + +.message { + margin: 0; + padding: 10px 12px; + border-radius: 10px; + font-size: 0.9rem; + font-weight: 600; +} + +.message.error { + background: #fdecec; + color: #9f1239; + border: 1px solid #f5c2c7; +} + +.message.success { + background: #eaf7ef; + color: #136a39; + border: 1px solid #b7e1c5; +} + +@media (max-width: 900px) { + .profile-picture-widget { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/prINTech-front/src/app/profile/profile-picture/profile-picture.html b/prINTech-front/src/app/profile/profile-picture/profile-picture.html new file mode 100644 index 0000000..5a28cd0 --- /dev/null +++ b/prINTech-front/src/app/profile/profile-picture/profile-picture.html @@ -0,0 +1,26 @@ +
+ + +
+ Photo de profil + +
+ +
+

{{ fileName || 'Aucun fichier sélectionné' }}

+ +
+ +
+ + @if (errorMessage) { +

{{ errorMessage }}

+ } + + @if (successMessage) { +

{{ successMessage }}

+ } +
+
\ No newline at end of file diff --git a/prINTech-front/src/app/profile/profile-picture/profile-picture.spec.ts b/prINTech-front/src/app/profile/profile-picture/profile-picture.spec.ts new file mode 100644 index 0000000..fbfa957 --- /dev/null +++ b/prINTech-front/src/app/profile/profile-picture/profile-picture.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProfilePicture } from './profile-picture'; + +describe('ProfilePicture', () => { + let component: ProfilePicture; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ProfilePicture], + }).compileComponents(); + + fixture = TestBed.createComponent(ProfilePicture); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/prINTech-front/src/app/profile/profile-picture/profile-picture.ts b/prINTech-front/src/app/profile/profile-picture/profile-picture.ts new file mode 100644 index 0000000..8b67508 --- /dev/null +++ b/prINTech-front/src/app/profile/profile-picture/profile-picture.ts @@ -0,0 +1,70 @@ +import { HttpClient } from '@angular/common/http'; +import { Component, inject } from '@angular/core'; +import { finalize } from 'rxjs'; +import { AvatarStateService } from '../../services/avatar-state'; + +@Component({ + selector: 'app-profile-picture', + imports: [], + templateUrl: './profile-picture.html', + styleUrl: './profile-picture.css', +}) +export class ProfilePicture { + private readonly http = inject(HttpClient); + private readonly avatarState = inject(AvatarStateService); + + fileName = ''; + errorMessage = ''; + successMessage = ''; + loading = false; + private selectedFile: File | null = null; + private readonly uploadUrl = '/api/v1/user/me/'; + + avatarSrc = this.avatarState.avatarSrc; + + onFileSelected(event: Event): void { + const input = event.target as HTMLInputElement; + const file = input.files?.[0] ?? null; + + this.errorMessage = ''; + this.successMessage = ''; + + if (file) { + this.fileName = file.name; + this.selectedFile = file; + + + this.uploadSelectedFile(); + return; + } + + this.fileName = ''; + this.selectedFile = null; + } + + private uploadSelectedFile(): void { + if (!this.selectedFile || this.loading) { + return; + } + + this.errorMessage = ''; + this.successMessage = ''; + this.loading = true; + + const formData = new FormData(); + formData.append('profile_picture', this.selectedFile); + + this.http + .patch(this.uploadUrl, formData) + .pipe(finalize(() => (this.loading = false))) + .subscribe({ + next: () => { + this.successMessage = 'Photo envoyée avec succès.'; + this.avatarState.refresh(); + }, + error: () => { + this.errorMessage = 'Échec de l\'upload. Vérifie l\'IP ou le backend.'; + }, + }); + } +} \ No newline at end of file diff --git a/prINTech-front/src/app/profile/profile.css b/prINTech-front/src/app/profile/profile.css new file mode 100644 index 0000000..f9d93d0 --- /dev/null +++ b/prINTech-front/src/app/profile/profile.css @@ -0,0 +1,188 @@ +.profile-page { + max-width: 1100px; + margin: 0 auto; + padding: 24px 12px 32px; + display: grid; + gap: 16px; +} + +.profile-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + background: #ffffff; + border: 1px solid #d8deea; + border-radius: 14px; + padding: 18px 20px; + box-shadow: 0 8px 24px rgba(9, 92, 156, 0.08); +} + +.profile-header h1 { + margin: 0; + font-size: clamp(1.4rem, 2.4vw, 2rem); + font-weight: 800; + color: #095c9c; +} + +.profile-header p { + margin: 6px 0 0; + color: #475569; +} + +.status-pill { + padding: 6px 12px; + border-radius: 999px; + font-size: 0.78rem; + font-weight: 700; + background: #eaf7ef; + color: #136a39; + border: 1px solid #b7e1c5; + white-space: nowrap; +} + +.profile-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} + +.profile-grid > .profile-card:first-child { + grid-column: 1 / -1; +} + +app-change-password { + grid-column: 1 / -1; +} + +.profile-card { + background: #ffffff; + border: 1px solid #d8deea; + border-radius: 14px; + padding: 18px 20px; + box-shadow: 0 8px 24px rgba(9, 92, 156, 0.08); + display: grid; + gap: 12px; +} + +.profile-card h2 { + margin: 0; + font-size: 1.05rem; + font-weight: 800; + color: #111827; +} + +.info-list { + display: grid; + gap: 10px; +} + +.info-item, +.pref-item { + display: flex; + justify-content: space-between; + gap: 12px; + padding: 10px 0; + border-bottom: 1px solid #eef2f7; +} + +.info-item:last-child, +.pref-item:last-child { + border-bottom: none; +} + +.avatar-block { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.avatar-placeholder { + width: 84px; + height: 84px; + border-radius: 999px; + background: #f8fbff; + border: 1px solid #d8deea; + color: #095c9c; + font-weight: 800; + font-size: 1.2rem; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.actions-row { + padding-top: 6px; + display: flex; + justify-content: flex-end; +} + +.action-btn { + padding: 10px 16px; + border: 2px solid transparent; + border-radius: 999px; + background: #095c9c; + color: #ffffff; + text-decoration: none; + font-size: 0.9rem; + font-weight: 700; + cursor: pointer; + transition: transform 0.15s ease, background-color 0.2s ease, border-color 0.2s ease; +} + +.action-btn:hover { + background: #074b7d; + text-decoration: none; + transform: translateY(-1px); +} + +.action-btn:focus-visible { + outline: none; + border-color: #f8cc08; +} + +.label { + font-size: 0.84rem; + font-weight: 700; + letter-spacing: 0.02em; + color: #5f6f92; + text-transform: uppercase; +} + +.value { + font-size: 0.95rem; + font-weight: 700; + color: #1f2937; + text-align: right; +} + +@media (max-width: 900px) { + .profile-grid { + grid-template-columns: 1fr; + } + + .profile-grid > .profile-card:first-child { + grid-column: auto; + } + + app-change-password { + grid-column: auto; + } + + .profile-header { + flex-direction: column; + align-items: flex-start; + } + + .avatar-block { + flex-direction: column; + align-items: flex-start; + } +} + +@media (max-width: 640px) { + .profile-page { + padding-inline: 4px; + } +} diff --git a/prINTech-front/src/app/profile/profile.html b/prINTech-front/src/app/profile/profile.html new file mode 100644 index 0000000..686f305 --- /dev/null +++ b/prINTech-front/src/app/profile/profile.html @@ -0,0 +1,41 @@ +
+
+
+

Mon profil

+

Gérez vos informations de compte.

+
+ Compte actif +
+ +
+
+

Compte

+
+ +
+
+
+ Nom d'utilisateur + {{username()}} +
+
+ Crédit disponible + {{credit()}}€ +
+
+ Email + {{email()}} +
+
+ Admin + {{ isAdmin() ? 'Oui' : 'Non' }} +
+
+ +
+ + +
+
\ No newline at end of file diff --git a/prINTech-front/src/app/profile/profile.spec.ts b/prINTech-front/src/app/profile/profile.spec.ts new file mode 100644 index 0000000..61d199d --- /dev/null +++ b/prINTech-front/src/app/profile/profile.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Profile } from './profile'; + +describe('Profile', () => { + let component: Profile; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Profile], + }).compileComponents(); + + fixture = TestBed.createComponent(Profile); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/prINTech-front/src/app/profile/profile.ts b/prINTech-front/src/app/profile/profile.ts new file mode 100644 index 0000000..024b458 --- /dev/null +++ b/prINTech-front/src/app/profile/profile.ts @@ -0,0 +1,19 @@ +import { Component, inject, computed } from '@angular/core'; +import { AuthService } from '../services/auth'; +import { ChangePassword } from './change-password/change-password'; +import { ProfilePicture } from './profile-picture/profile-picture'; + +@Component({ + selector: 'app-profile', + imports: [ChangePassword, ProfilePicture], + templateUrl: './profile.html', + styleUrl: './profile.css', +}) +export class Profile { + private readonly auth = inject(AuthService); + + username = computed(() => this.auth.currentUser()?.username ?? ''); + email = computed(() => this.auth.currentUser()?.email ?? ''); + credit = computed(() => this.auth.currentUser()?.credit ?? 0); + isAdmin = computed(() => this.auth.currentUser()?.is_staff ?? false); +} \ No newline at end of file diff --git a/prINTech-front/src/app/services/admin.ts b/prINTech-front/src/app/services/admin.ts new file mode 100644 index 0000000..7ea36f7 --- /dev/null +++ b/prINTech-front/src/app/services/admin.ts @@ -0,0 +1,200 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable, inject } from '@angular/core'; +import { Observable, map } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class AdminService { + private readonly http = inject(HttpClient); + private readonly apiBase = '/api/v1'; + + // ── Requests ──────────────────────────────────────────────── + getWaitingRequests(): Observable { + return this.http + .get>(`${this.apiBase}/admin/requests/`) + .pipe(map((response) => Array.isArray(response) ? response : response.results)); + } + + changeRequestStatus(requestId: string, status: PrintRequestStatus, price?: number): Observable { + const body: Record = { status }; + if (price !== undefined) body['price'] = price; + return this.http.patch( + `${this.apiBase}/admin/requests/${requestId}/change_status/`, + body + ); + } + + // ── Users ─────────────────────────────────────────────────── + getUsers(): Observable { + return this.http + .get>(`${this.apiBase}/admin/users/`) + .pipe(map((response) => Array.isArray(response) ? response : response.results)); + } + + createUser(payload: AdminUserCreatePayload): Observable { + return this.http.post(`${this.apiBase}/admin/users/`, payload); + } + + updateUser(userId: string, payload: AdminUserUpdatePayload): Observable { + return this.http.patch(`${this.apiBase}/admin/users/${userId}/`, payload); + } + + deleteUser(userId: string): Observable { + return this.http.delete(`${this.apiBase}/admin/users/${userId}/`); + } + + // ── Operations ────────────────────────────────────────────── + getOperations(): Observable { + return this.http + .get>(`${this.apiBase}/admin/operations/`) + .pipe(map((response) => Array.isArray(response) ? response : response.results)); + } + + createOperation(payload: OperationCreatePayload): Observable { + return this.http.post(`${this.apiBase}/admin/operations/`, payload); + } + + // ── Filaments ─────────────────────────────────────────────── + getFilaments(): Observable { + return this.http + .get>(`${this.apiBase}/admin/filaments/`) + .pipe(map((response) => Array.isArray(response) ? response : response.results)); + } + + createFilament(payload: FilamentPayload): Observable { + return this.http.post(`${this.apiBase}/admin/filaments/`, payload); + } + + updateFilament(id: number, payload: FilamentPayload): Observable { + return this.http.patch(`${this.apiBase}/admin/filaments/${id}/`, payload); + } + + deleteFilament(id: number): Observable { + return this.http.delete(`${this.apiBase}/admin/filaments/${id}/`); + } + + // ── Printers ──────────────────────────────────────────────── + getPrinters(): Observable { + return this.http + .get>(`${this.apiBase}/admin/printers/`) + .pipe(map((response) => Array.isArray(response) ? response : response.results)); + } + + updatePrinter(name: string, payload: PrinterUpdatePayload): Observable { + return this.http.patch(`${this.apiBase}/admin/printers/${name}/`, payload); + } +} + +// ── Shared ───────────────────────────────────────────────────── +interface PaginatedResponse { + count: number; + next: string | null; + previous: string | null; + results: T[]; +} + +// ── Request types ─────────────────────────────────────────────── +export type PrintRequestStatus = + | 'SUBMITTED' + | 'AWAITING_PAYMENT' + | 'PENDING' + | 'PRINTING' + | 'AWAITING_PICKUP' + | 'PICKED_UP' + | 'FAILED' + | 'CANCELED'; + +export interface PrintRequest { + id: string; + user: string; + status: PrintRequestStatus; + created_at: string; + price: number; + comment: string | null; + printer: string | null; + file: { + path: string; + number_of_printing: number; + filament: number | null; + para_slicer: unknown; + } | null; +} + +// ── User types ────────────────────────────────────────────────── +export interface AdminUser { + id: string; + username: string; + email: string; + credit: number; + priority: string; + is_staff: boolean; + is_active: boolean; + role: string; + +} + +export interface AdminUserCreatePayload { + username: string; + email: string; + password: string; + is_staff: boolean; + is_active: boolean; + role?: string; +} + +export interface AdminUserUpdatePayload { + username?: string; + email?: string; + password?: string; + priority?: string; + is_staff?: boolean; + is_active?: boolean; + role?: string; +} + +// ── Operation types ───────────────────────────────────────────── +export type OperationType = 'CASH' | 'CARD' | 'PAYMENT' | 'REFUND'; + +export interface Operation { + id: string; + beneficiary: string; + agent: string; + amount: number; + operation_type: OperationType; + comment: string | null; + created_at: string; + request: string | null; +} + +export interface OperationCreatePayload { + beneficiary: string; + amount: number; + operation_type: OperationType; + request?: string; + comment?: string; +} + +// ── Filament types ────────────────────────────────────────────── +export interface Filament { + id: number; + color: string; + color_name: string; + type: 'PLA' | 'PETG'; + quantity: number; + price: number; +} + +export type FilamentPayload = Omit; + +// ── Printer types ─────────────────────────────────────────────── +export type PrinterStatus = 'UP' | 'DOWN' | 'USED'; + +export interface Printer { + name: string; + status: PrinterStatus; +} + +export interface PrinterUpdatePayload { + status: PrinterStatus; +} diff --git a/prINTech-front/src/app/services/auth.spec.ts b/prINTech-front/src/app/services/auth.spec.ts new file mode 100644 index 0000000..f441b2f --- /dev/null +++ b/prINTech-front/src/app/services/auth.spec.ts @@ -0,0 +1,20 @@ +import { TestBed } from '@angular/core/testing'; +import { provideHttpClient } from '@angular/common/http'; + +import { AuthService } from './auth'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideHttpClient()], + }); + + service = TestBed.inject(AuthService); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/prINTech-front/src/app/services/auth.ts b/prINTech-front/src/app/services/auth.ts new file mode 100644 index 0000000..8365c62 --- /dev/null +++ b/prINTech-front/src/app/services/auth.ts @@ -0,0 +1,175 @@ +import { computed, Injectable, inject, signal } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, tap } from 'rxjs'; +import { jwtDecode } from 'jwt-decode'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + + private readonly http = inject(HttpClient); + private readonly accessTokenStorageKey = 'auth_access_token'; + private readonly refreshTokenStorageKey = 'auth_refresh_token'; + private readonly apiBase = '/api/v1'; + private readonly token = signal( + localStorage.getItem(this.accessTokenStorageKey) + ); + + private readonly userStorageKey = 'auth_current_user'; + // Current user cached as a signal so UI can reactively depend on it + currentUser = signal( + (() => { + const stored = localStorage.getItem(this.userStorageKey); + return stored ? JSON.parse(stored) : null; + })() + ); + + readonly isAuthenticated = computed(() => this.token() !== null); + + constructor() { + const token = this.token(); + if (token) { + try { + const decoded: any = jwtDecode(token as string); + const now = Date.now() / 1000; + if (!decoded.exp || decoded.exp < now) { + this.clearToken(); + } else { + this.loadCurrentUser().subscribe({ next: () => {}, error: () => {} }); + } + } catch (e) { + this.clearToken(); + } + } + } + + login(email: string, password: string): Observable { + const payload: LoginRequest = { email, password }; + + return this.http + .post(`${this.apiBase}/token/`, payload) + .pipe( + tap(({ access, refresh }) => { + this.setTokens(access, refresh); + // eagerly load current user + this.loadCurrentUser().subscribe({ next: () => {}, error: () => {} }); + }) + ); + } + + getCurrentUser(): Observable { + return this.http.get(`${this.apiBase}/user/me/`); + } + + refreshToken(): Observable{ + const refresh = localStorage.getItem(this.refreshTokenStorageKey); + if (!refresh) throw new Error('no refresh token'); + return this.http + .post(`${this.apiBase}/token/refresh/`, { refresh }) + .pipe(tap(({ access }) => this.setTokens(access, refresh))); + } + + change_password( + old_password: string, + new_password: string, + confirm_password: string + ): Observable { + const change_password_payload: ChangePasswordRequest = { + old_password, + new_password, + confirm_password, + }; + + return this.http.patch( + `${this.apiBase}/user/me/change-password/`, + change_password_payload + ); + } + logout(): void { + this.clearToken(); + this.currentUser.set(null); + } + + getToken(): string | null { + return this.token(); + } + + isLoggedIn(): boolean { + const token = this.token(); + + if (token == null) { + console.warn('no token') + return false; + } + + const decodedToken = jwtDecode(token); + + if (!decodedToken.exp) { + console.warn('invalid token') + return false; + } + const currentTime = Date.now() / 1000; + const isExpired = decodedToken.exp < currentTime; + + return (!isExpired); + } + + private setTokens(accessToken: string, refreshToken: string): void { + this.token.set(accessToken); + localStorage.setItem(this.accessTokenStorageKey, accessToken); + localStorage.setItem(this.refreshTokenStorageKey, refreshToken); + } + + private clearToken(): void { + this.token.set(null); + localStorage.removeItem(this.accessTokenStorageKey); + localStorage.removeItem(this.refreshTokenStorageKey); + localStorage.removeItem(this.userStorageKey); + } + + loadCurrentUser(): Observable { + const obs = this.http.get(`${this.apiBase}/user/me/`); + obs.subscribe({ + next: (u) => { + this.currentUser.set(u); + localStorage.setItem(this.userStorageKey, JSON.stringify(u)); + }, + error: () => this.currentUser.set(null) + }); + return obs; + } +} + + +interface LoginRequest { + email: string; + password: string; +} + +interface LoginResponse { + access: string; + refresh: string; +} + +interface RefreshResponse { + access: string; +} +interface ChangePasswordRequest { + old_password: string; + new_password: string; + confirm_password: string; +} + +interface ChangePasswordResponse { + message: string; +} + +interface UserMeResponse { + id: string; + username: string; + email: string; + credit: number; + is_staff: boolean; + profile_picture: string | null; +} diff --git a/prINTech-front/src/app/services/avatar-state.ts b/prINTech-front/src/app/services/avatar-state.ts new file mode 100644 index 0000000..562df1e --- /dev/null +++ b/prINTech-front/src/app/services/avatar-state.ts @@ -0,0 +1,17 @@ +import { Injectable, inject, computed } from '@angular/core'; +import { AuthService } from './auth'; + +@Injectable({ providedIn: 'root' }) +export class AvatarStateService { + private readonly auth = inject(AuthService); + private readonly defaultAvatar = 'profile.svg'; + + readonly avatarSrc = computed(() => { + const pic = this.auth.currentUser()?.profile_picture; + return pic ? pic : this.defaultAvatar; + }); + + refresh(): void { + this.auth.loadCurrentUser().subscribe(); + } +} \ No newline at end of file diff --git a/prINTech-front/src/app/services/history-services.spec.ts b/prINTech-front/src/app/services/history-services.spec.ts new file mode 100644 index 0000000..2399d0d --- /dev/null +++ b/prINTech-front/src/app/services/history-services.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { HistoryServices } from './history-services'; + +describe('HistoryServices', () => { + let service: HistoryServices; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(HistoryServices); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/prINTech-front/src/app/services/history-services.ts b/prINTech-front/src/app/services/history-services.ts new file mode 100644 index 0000000..d92ee40 --- /dev/null +++ b/prINTech-front/src/app/services/history-services.ts @@ -0,0 +1,37 @@ +import { Injectable, inject } from '@angular/core'; +import { HistoryItem } from '../history/history.model'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import id from '@angular/common/locales/extra/id'; +export interface Filament { + id: number; + color: string; + color_name: string; + type: string; + quantity: number; +} + +@Injectable({ + providedIn: 'root', +}) +export class HistoryServices { + private readonly apiBase = '/api/v1'; + private readonly http = inject(HttpClient) + + getHistory(): Observable { + return this.http.get(`${this.apiBase}/requests/`) + } + + getFilaments(): Observable { + return this.http.get(`${this.apiBase}/filaments/`) + } + + relaunchRequest(requestId: string): Observable { + return this.http.post(`${this.apiBase}/requests/${requestId}/relaunch/`, {}); + } + + payRequest(id: string): Observable { + return this.http.post(`/api/v1/requests/${id}/pay/`, {}); + } +} + diff --git a/prINTech-front/src/app/services/home.spec.ts b/prINTech-front/src/app/services/home.spec.ts new file mode 100644 index 0000000..261e6ec --- /dev/null +++ b/prINTech-front/src/app/services/home.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { HomeService } from './home'; + +describe('Home', () => { + let service: HomeService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(HomeService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/prINTech-front/src/app/services/home.ts b/prINTech-front/src/app/services/home.ts new file mode 100644 index 0000000..4aa1334 --- /dev/null +++ b/prINTech-front/src/app/services/home.ts @@ -0,0 +1,104 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable, signal, } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class HomeService { + private readonly http = inject(HttpClient) + private readonly ApiBase = '/api/v1' + + userCredit = signal(null); + active_printers = signal(0); + total_printers = signal(67); + username = signal('John Doe'); + printers_status = signal('Disponible'); + email = signal('johndoe@gmail.com'); + is_active = signal(true); + queue_size = signal(67); + + getActivePrinters(){ + this.http.get(`${this.ApiBase}/printers/`) + .subscribe( + (res) => { + this.total_printers.set(res.length) + const active = res.filter( (stat) => stat.status == "UP").length + this.active_printers.set(active) + if (active == res.length){ + this.printers_status.set('Complet') + } else { + this.printers_status.set('Disponible') + } + } + ) + } + + loadUserInfo(){ + this.http.get< User >(`${this.ApiBase}/user/me`) + .subscribe( + (res) => { + this.username.set(res.username) + this.email.set(res.email) + this.userCredit.set(res.credit) + this.is_active.set(res.is_active) + } + ) + } + +GetQueue() { + this.http.get(`${this.ApiBase}/requests/`) + .subscribe( + (res) => { + // Filter out requests that are done, cancelled, or failed + const inProgressRequests = res.filter(request => + request.status !== 'PICKED_UP' && + request.status !== 'CANCELED' && + request.status !== 'FAILED' + ); + + // Set the queue size to only show active requests + this.queue_size.set(inProgressRequests.length); + } + ); + } +} + +export interface Printer { + name: string, + status: string, +} + +export interface User { + id: string, + username: string, + email: string, + credit: number, + is_active: boolean; + +} + +export interface Request{ + id: string, + user: string, + file: { + path: string, + number_of_printing: number, + filament: number //weird as fuck + para_slicer: string + }, + printer: string, + comment: string, + created_at: Date, + status: string, +} + +// export interface Request{ +// id: string, +// user: string, +// file_path: string, +// number: number, +// filament: string, +// comment: string, +// created_at: Date, +// status: string, +// } diff --git a/prINTech-front/src/app/services/print.spec.ts b/prINTech-front/src/app/services/print.spec.ts new file mode 100644 index 0000000..695cb79 --- /dev/null +++ b/prINTech-front/src/app/services/print.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { Print } from './print'; + +describe('Print', () => { + let service: Print; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(Print); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/prINTech-front/src/app/services/print.ts b/prINTech-front/src/app/services/print.ts new file mode 100644 index 0000000..01a49d6 --- /dev/null +++ b/prINTech-front/src/app/services/print.ts @@ -0,0 +1,57 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class Print { + private readonly http = inject(HttpClient); + private readonly ApiBase = '/api/v1'; + + getFilaments(): Observable { + return this.http.get(`${this.ApiBase}/filaments/`); + } + + SendRequest(payload: PrintRequestPayload): Observable { + const formData = new FormData(); + + formData.append('filament', String(payload.filament)); + formData.append('comment', payload.comment ?? ''); + formData.append('path', payload.path); + formData.append('number_of_printing', String(payload.number_of_printing)); + + if (payload.para_slicer !== undefined) { + formData.append('para_slicer', JSON.stringify(payload.para_slicer)); + } + + return this.http.post(`${this.ApiBase}/requests/`, formData); + } + +} + +export interface Filament { + id: number; + color: string; + color_name: string; + type: string; + quantity: number; +} + +export interface PrintRequestPayload { + filament: number; + comment: string; + path: File; + number_of_printing: number; + para_slicer?: Record; +} + +export interface PrintRequestResponse { + id: string; + user: string; + printer: string | null; + filament: number; + comment: string | null; + created_at: string; + status: string; +} \ No newline at end of file diff --git a/prINTech-front/src/app/services/token-interceptor.spec.ts b/prINTech-front/src/app/services/token-interceptor.spec.ts new file mode 100644 index 0000000..26c89c7 --- /dev/null +++ b/prINTech-front/src/app/services/token-interceptor.spec.ts @@ -0,0 +1,17 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpInterceptorFn } from '@angular/common/http'; + +import { tokenInterceptor } from './token-interceptor'; + +describe('tokenInterceptor', () => { + const interceptor: HttpInterceptorFn = (req, next) => + TestBed.runInInjectionContext(() => tokenInterceptor(req, next)); + + beforeEach(() => { + TestBed.configureTestingModule({}); + }); + + it('should be created', () => { + expect(interceptor).toBeTruthy(); + }); +}); diff --git a/prINTech-front/src/app/services/token-interceptor.ts b/prINTech-front/src/app/services/token-interceptor.ts new file mode 100644 index 0000000..37a88cf --- /dev/null +++ b/prINTech-front/src/app/services/token-interceptor.ts @@ -0,0 +1,36 @@ +import { HttpInterceptorFn, HttpRequest, HttpHandlerFn } from '@angular/common/http'; +import { inject } from '@angular/core'; +import { catchError, switchMap, throwError } from 'rxjs'; +import { AuthService } from './auth'; + +export const tokenInterceptor: HttpInterceptorFn = (req: HttpRequest, next: HttpHandlerFn) => { + const authService = inject(AuthService); + + if (req.url.includes('/api/v1/token/')) { + return next(req); + } + + const token = authService.getToken(); + if (!token) { + return next(req); + } + + const requestWithToken = (accessToken: string) => + req.clone({ + setHeaders: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + if (!authService.isLoggedIn()) { + return authService.refreshToken().pipe( + switchMap(({ access }) => next(requestWithToken(access))), + catchError((error) => { + authService.logout(); + return throwError(() => error); + }) + ); + } + + return next(requestWithToken(token)); +}; diff --git a/prINTech-front/src/index.html b/prINTech-front/src/index.html new file mode 100644 index 0000000..e50a599 --- /dev/null +++ b/prINTech-front/src/index.html @@ -0,0 +1,13 @@ + + + + + Ppp + + + + + + + + diff --git a/prINTech-front/src/main.ts b/prINTech-front/src/main.ts new file mode 100644 index 0000000..5df75f9 --- /dev/null +++ b/prINTech-front/src/main.ts @@ -0,0 +1,6 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { App } from './app/app'; + +bootstrapApplication(App, appConfig) + .catch((err) => console.error(err)); diff --git a/prINTech-front/src/styles.css b/prINTech-front/src/styles.css new file mode 100644 index 0000000..d78b3d5 --- /dev/null +++ b/prINTech-front/src/styles.css @@ -0,0 +1,11 @@ +@import url('https://fonts.googleapis.com/css2?family=Exo+2:wght@400;500;600;700&display=swap'); + +html, body { + margin: 0; + padding: 0; + font-family: 'Exo 2', sans-serif; +} + +* { + outline: none; +} \ No newline at end of file diff --git a/prINTech-front/tests/auth.setup.ts b/prINTech-front/tests/auth.setup.ts new file mode 100644 index 0000000..3732b2d --- /dev/null +++ b/prINTech-front/tests/auth.setup.ts @@ -0,0 +1,16 @@ +import { test as setup, expect } from '@playwright/test'; + +const authFile = 'playwright/.auth/user.json'; + +setup('authenticate', async ({ page }) => { + await page.goto('/login'); + await page.getByLabel('Email').fill('testuser@example.com'); + await page.getByLabel('Password').fill('CorrectPassword123!'); + await page.getByRole('button', { name: /log in/i }).click(); + + // Wait for application navigation after login + await page.waitForURL(/\/dashboard|\/home/); + + // Save storage state (cookies + local storage) + await page.context().storageState({ path: authFile }); +}); \ No newline at end of file diff --git a/prINTech-front/tests/auth.spec.ts b/prINTech-front/tests/auth.spec.ts new file mode 100644 index 0000000..952c39c --- /dev/null +++ b/prINTech-front/tests/auth.spec.ts @@ -0,0 +1,72 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Authentication Flow', () => { + test.beforeEach(async ({ page }) => { + // Intercept API calls to prevent real network requests failing the E2E tests + await page.route('**/api/v1/token/', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + access: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyBleHAiOjQxMDI0NDQ4MDAsInVzZXJfaWQiOjF9.signature', + refresh: 'mock-refresh-token' + }) + }); + }); + + await page.route('**/api/v1/user/me/', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + id: 1, + email: 'testuser@example.com', + is_staff: false + }) + }); + }); + + // Mock the print-jobs endpoint accessed immediately by the HomeComponent on redirect + await page.route('**/api/v1/print-jobs/', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([]) + }); + }); + + await page.goto('/login'); + }); + + test('should successfully log in with valid credentials', async ({ page }) => { + await page.getByLabel('Adresse e-mail').fill('testuser@example.com'); + await page.getByLabel('Mot de passe').fill('CorrectPassword123!'); + + // Click the submission button + await page.getByRole('button', { name: 'Se connecter' }).click(); + + // Verify successful redirection to home page + await expect(page).toHaveURL('/'); + }); + + test('should display error message on invalid login', async ({ page }) => { + // Override token route to simulate a 401 Unauthorized response + await page.unroute('**/api/v1/token/'); + await page.route('**/api/v1/token/', async (route) => { + await route.fulfill({ + status: 401, + contentType: 'application/json', + body: JSON.stringify({ detail: 'No active account found with the given credentials.' }) + }); + }); + + await page.getByLabel('Adresse e-mail').fill('wrong@example.com'); + await page.getByLabel('Mot de passe').fill('WrongPassword!'); + await page.getByRole('button', { name: 'Se connecter' }).click(); + + // Assert against the alert container text + const errorMessage = page.getByRole('alert'); + await expect(errorMessage).toBeVisible(); + await expect(errorMessage).toContainText('Adresse e-mail ou mot de passe invalide.'); + }); +}); \ No newline at end of file diff --git a/prINTech-front/tests/example.spec.ts b/prINTech-front/tests/example.spec.ts new file mode 100644 index 0000000..54a906a --- /dev/null +++ b/prINTech-front/tests/example.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); +}); + +test('get started link', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Click the get started link. + await page.getByRole('link', { name: 'Get started' }).click(); + + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +}); diff --git a/prINTech-front/tests/profile.spec.ts b/prINTech-front/tests/profile.spec.ts new file mode 100644 index 0000000..3bcc2be --- /dev/null +++ b/prINTech-front/tests/profile.spec.ts @@ -0,0 +1,62 @@ +import { test, expect } from '@playwright/test'; + +test.describe('User Profile & Security', () => { + test.beforeEach(async ({ page }) => { + // Inject valid localStorage entries matching your AuthService keys precisely + await page.addInitScript(() => { + const futureJwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyBleHAiOjQxMDI0NDQ4MDAsInVzZXJfaWQiOjF9.signature'; + localStorage.setItem('access_token', futureJwt); + localStorage.setItem('refresh_token', 'mock-refresh-token'); + localStorage.setItem('current_user', JSON.stringify({ + id: 1, + email: 'testuser@example.com', + is_staff: false + })); + }); + + // Intercept the HTTP profile fetch initialized by profile component hooks or interceptors + await page.route('**/api/v1/user/me/', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + id: 1, + email: 'testuser@example.com', + is_staff: false + }) + }); + }); + + // Mock the password change endpoint response + await page.route('**/api/v1/user/change-password/', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ message: 'Password updated successfully' }) + }); + }); + + await page.goto('/profile'); + }); + + test('should display current user details correctly', async ({ page }) => { + const emailText = page.getByText('testuser@example.com'); + await expect(emailText).toBeVisible(); + }); + + test('should change password successfully', async ({ page }) => { + // This button will now successfully show because user info initializes correctly + await page.getByRole('button', { name: 'Modifier le mot de passe' }).click(); + + // Fill out the modal fields + await page.getByLabel('Ancien mot de passe').fill('CorrectPassword123!'); + await page.getByLabel('Nouveau mot de passe').fill('NewSecurePassword123!'); + await page.getByLabel('Confirmer le nouveau mot de passe').fill('NewSecurePassword123!'); + + // Target the correct submit button label + await page.getByRole('button', { name: 'Enregistrer' }).click(); + + // Confirm the modal closes + await expect(page.getByLabel('Ancien mot de passe')).not.toBeVisible(); + }); +}); \ No newline at end of file diff --git a/prINTech-front/tsconfig.app.json b/prINTech-front/tsconfig.app.json new file mode 100644 index 0000000..264f459 --- /dev/null +++ b/prINTech-front/tsconfig.app.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "src/**/*.spec.ts" + ] +} diff --git a/prINTech-front/tsconfig.json b/prINTech-front/tsconfig.json new file mode 100644 index 0000000..2ab7442 --- /dev/null +++ b/prINTech-front/tsconfig.json @@ -0,0 +1,33 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/prINTech-front/tsconfig.spec.json b/prINTech-front/tsconfig.spec.json new file mode 100644 index 0000000..d383706 --- /dev/null +++ b/prINTech-front/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "vitest/globals" + ] + }, + "include": [ + "src/**/*.d.ts", + "src/**/*.spec.ts" + ] +} diff --git a/pyproject.toml b/pyproject.toml index a936b9f..8cefb0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,5 +11,6 @@ dependencies = [ "djangorestframework>=3.16.1", "djangorestframework-simplejwt>=5.5.1", "drf-spectacular>=0.29.0", + "pillow>=12.2.0", "psycopg2-binary>=2.9.11", ] diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..5fca3f8 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "failed", + "failedTests": [] +} \ No newline at end of file diff --git a/uv.lock b/uv.lock index 2100f6f..bfe7b24 100644 --- a/uv.lock +++ b/uv.lock @@ -138,6 +138,64 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, +] + [[package]] name = "printech-back" version = "0.2.0" @@ -149,6 +207,7 @@ dependencies = [ { name = "djangorestframework" }, { name = "djangorestframework-simplejwt" }, { name = "drf-spectacular" }, + { name = "pillow" }, { name = "psycopg2-binary" }, ] @@ -160,6 +219,7 @@ requires-dist = [ { name = "djangorestframework", specifier = ">=3.16.1" }, { name = "djangorestframework-simplejwt", specifier = ">=5.5.1" }, { name = "drf-spectacular", specifier = ">=0.29.0" }, + { name = "pillow", specifier = ">=12.2.0" }, { name = "psycopg2-binary", specifier = ">=2.9.11" }, ]