diff --git a/.abacus.donotdelete b/.abacus.donotdelete new file mode 100644 index 0000000..c7002ee --- /dev/null +++ b/.abacus.donotdelete @@ -0,0 +1 @@ +gAAAAABqViYyd3d0F4BEcAOUTgo-sXGPwqHKtcb9FIxeSulmTeVeKePkx8d3lxX6sbX2PEVoMK53ZC9G318UpxdHLvc0e7gPjlw-4P0Jl_5nRhv4YNyLV7RhIMcXCBmHQhgo-GvkU_ksMGQ2S1AxYPXKEXTyi4bsGp4ftIMxd4c6HoHZzDqe9z4Shhx8CCA6jFQYBf4ilqVM0dBBxCSTOLabaI5L4kilHP6LhWIEiYy0Gv0CBmoJYosziTGrq0XzgHxnQNO1HJeDOAj3e9EFmGKKUwSRKIpVUM6Sg1yOG4qQyZcAZFT9glBAiTMn9jI0VCAkVux9dPsLmSHF-PVsfCc1Ru6cOI7_0f9OoYMxyJzhuSbYwf-6RZxxsdPmYJyxv-BoCd9rlMgBZutrq7HtJRWuS-qfwS6VWXDg_koCCjhPxTGp0kv6A4f-yzRcTN9MIGDLpyqDzU0QSmSQzoJyv7_uopFSGJZinQrN77JehXHmzZ-LtA-kPup4BfzuasrnW0iWGVwSOrBSrRmL_0y5M939TiHX7WPshaeCaDC_alhyCr7OpEwYp_WAmIh9ePPU6eUQI8WuE7YPzJTfcAbX5_M0R7GIaWAToQxpb4ZoOsK8fvo3mOzUnA8QMlPA7QcMP7MhXjszLi8DbzaroNDGT0xMA0njBBuS2EsnszllcvhH4M-3OMI7TlDQVAiudt8GjtXWaBB636jhKyKXKCY5fhfbCXmA81XAHZYLTkGBKlg7BwVkkQMK_4L-PCAbJNO3s_hgSUDw1F3PFmETH_kNS9tB4tGT-ZwbLizSy4QC5hCH7R8eCZM7nWuG-BXRUy5rAJ5DI_mHx2Q_yZS3KHckbglraPs6TDrFZ5JtaJFJpa5oSYwjCBBxrFu9s4-sLoPn8373oh3443mhGslnC_T_OCYKjfYF-nMzaQBmqm3FMwx59yWPdNzFeG95t86jVL7MTwgeGdU4qdKzm7Oz7bukrcfKHuPZUydiIvjShPmKd6HOJR438jyvBxWEUjkuwFvhDjBavTjlRjaDJrFTPooRhiY10i4XoATB8UdjFaz7kOSGHJ6Dm_uKga-fH1Y2-1ZHapRe3v-CH5VzxGDqlZJqdFYCtG7PvsJLgUouOBrg0dP2yxjaqlff0vSgiDqWyLauHqrW5xZlX1RTrQ64r6av6i2IGdsUTkgguAXdzm4UIMNYsNvY-e6GV9nAVZDatXAptcjSWlSGLg4UyZZF1L5bDzXHTSfvLxwg30Sw5UjoVF-rdZjLZvmydto3LMBe4WiyydV0fpXtjn-ZF-w13y3fOBHlfkhI32j3VsROfOiWzjuYRKm2d6IJrf0NfkwcNKeB2FOZAdYFDnD9qOxL0fkfks6viwhByJAaTni6836pY_6ZMJgh3lKwXRcnMUxZxbNR_6qLKSnR_yUMlixVl6a2ZU1g0uW5EmVjITUKVsLe1uDuFG-yM6nYSWcsunXmGQ-2CavyXIrUtePWq-MVkvvlk-FAHleXQpvAEXuy1IwumEMuRC29Io7YEkOIrWAzVRBtwunPQMv8wyaSaj6iQPuiFB4Jit5hhZiTE83bFGf2noUNZjY2XK1b3f8t9GkZEaHOKria22UD4sthbO90_XsIMb7BuLdaWeaNA0jAkYbeu6a5_GlSgRSKpAAxJPAJsiSpfvI9ImJVu6QcQeFm_7usbFmFEpze9PwdZZxQ3UawLEriUYkZ0fAtz8UN1-Ybx-3fvvEPkHQEmglP_1aTbccdYgyccRyzb6fctoAS0U4AbSWrw4UViNbYcZxlOCotEDFWn-PHOq673kjl-hRXLSpIhBvQzc10LlYliY4Ki1alh7L87mZdL2auR5HIgAkg4Rw9HjrAskxLMJ1v22fNnc-Txf5ZoT3-7gxSMAI3i3a41NvydackXA377AqO4dnYalkQqhSe1TL7N_7tMZpsxf7ET_kMcLt5pptSiE0Pgn0N44xpKQM9W07cuXtEBLK9-5SnOCMjdQc-HL7zXHMNybeFJwNvTFLhoCbswqurPw0Np4ueCu66JhNjOtJSWS0w5Y2aAr1VzyNImO-TbUqRG6aE4SJEgEfgOFutROdXPd3_fNGNJepw0qnqkcPDzH9v9MleA0xfK5zt3iNO5M5KSkvnG7y4R-88d_v4nzw0Lvu0tEpmhe1u169QXHnvzhPbb9tV2TlNig2JCPSiY8dU72Kakmzr1DApIB_nSdYqncUit1Lmr7JqIK5ZvUbCEi37E5WOnRsp_lYQefJNLV3ZTiYh3VmaeoVlyh3xRK9QJ4pad8bY6C4YsCDxrahRkISsloK2XdTnsEbGP2zP0rHDWCcmNHjSVEXUDsIn6duuRZ0lcLrm3l73yXy6ZWe6ABFTS9Ffaub_PsPHxTG6AoG7-0QEmNaMvF8C-Vxu6IzISLkwjRLy1BRVBFt2q8zrZ0YZf77BYQ8zD9sqMmaCqudbyNRqAdfwu7E6fF353VVz6UMw8KhvIRp-Aho7iJSg073zbBsysAjZsc42ux2c10mvx02afjfX3D-y4KB7ox6JvfE-eEyxZxAmjYqxAT2hGntPSxvKtwgh0pD7gaVx07jgpyL6vmXWeyCVH3EmGBZaxRyOq4ITE87rxOA-rGd56gQsOfQPyTjtMwSjnwj-qtAlTMJsE_D9SzorWzHd6yw8_CCZPnMT7GcYDpK1bfbNNfaz0kRIhn3GRxlt95Ivy-RK71d-W3SW1QSkOrfhAx6OmJafO-awpgysT6w5BeEFhuXHCoYY5SGq0bwboxOp6WqGHMhNfdMVt4o5bi6hQBF0dMRVKOWxK4u-bf6VA4nzzAMN5R1mKxkJ0YW3UR3eOx6WlYqjgEmlGnufXH_19xJ6yMIbGC1MzK07Z7DijREJ0m931dQtl7jTXCYYAM5AhVtkMtvNrXuanDskARsXVl2XRlbhSrsgxTkusFCBCSP7-qeZGQkGiOuilT-FqdrsHsUkKKzSAnJfmRCKQMbNNm2AszLcslOjbCFGQzXlBBTcXXLcDWhM8_hs3MNuixx_bwQDDjmZ4-ZhMzSz6vyJashL9Gm0k74CT9blEiWgxg1FhUMaWtZ19T_4zxVEKOlXk63m9e9Z3311aSKVvQhrt0wweLsaMB5-ViqhCluTHqSdcDNHiosytnmNeXuzjPfZeXfJWKM8t1qdwOwz7kYKaYSrtioXcIMOvOBw4OOgFDEwKaQHR9quB1VWXTfKUI7S2DRyx2vf9lio04g-zfVir_VarobVgWvZV_hlL9nzR2c8z5wYb2eki9eAvUvURpH0rDKBKdGe5mc7ojdZKK7ZoKX_UQBEHwxI1oy_1dhzXfTORu82el1RLvb3ypvJK07hNFkPVXRjLtlEV6fM_iMsGZCT9p3xNaIAChi0QhKDX_17aZRtc_GuBvLJMTqbHFMixkH6nv18REzJBFwumMAZ_YYesiNPsm4esUBKqAtE6YhT6BCoIlLJXaFKyWg7zAPDwJfOk1qIQQdL-h8EslG-JODQd19t5TaUDNX7xhkU3OeAjzgyNCRvZuJBo8BIeWzBxuJW2WSARU8F0aLsGCGMicsmx92U6-ucjSWGE7D7_tmMdWuNXXq9H1ANBHlRyr6WvPJzgsZN7Twj6CWoUKxKptYhGohC1wckASy7OFGc_7_OJM_Sd_Ts3Zgy630nyK3UzpbTvtgbWl2x485RFEQS0qw4CVLJGlk2iCYvkrau2gfPmGhe9UyZZCSZ3xU6sM-gdLJg6cXUrk8h_JPcr6iawFG1NqG6f9hjFX_rme1W21TT21BqisBRZVJJcSgLL0SVZEsGAeRQJqFlnNqUygp_glO8pbrKFHKQ3-8l8o8tCcWi-wv8mIkcuA2c6rMKthXyGuMkZ46f8V81Furq7XSCHMfdy2mvGgPgmRgIKjiquFiQzYx2MrAd775SGpfNdJgY-U4ZGGp_uYe_YTI1h7j3VuhJxMsqc6FWnmE-AYVE7VlnMbvvHb6WM5N2AvkbmHemmqJth5atAEHkjXlQjx01H_RjEndL2wiL_1b-l2bjRB0Zh186KiVsoYokNpVWh5jFd2Yp-jR-BQVAnr2sbKXHywaaC7eDfmC8PXm6KWnnQj_74p-iekN2UAZIEOWuvyi3KmHxnwc4tkqps2TnwGcWwphP2bGCls43KjDBq8VCXZ4uCsqNoIGMhZGroJ3ux9NDwxYFki-rU3o7wMw8kRSaF1S7Y0o-11ZJJderke6yq9XNqho18VRms8ZpzrASBcaDNbbmgULPrfSRXTBvyT2mVlOjxFrELa87A4d2j07TXi336DDlpr4rHz5d-wI8zTfBekekgYwCBRJIdUdlfwDcD20sslGGf7vF-TY-EyNLnN_WOzd9gy-Gm39lZPwj9FstRfH0lJ4-GYzTWf6qIIQmPHxdBBgi22PO7HWHBIDGUhwYB2fUD3LUdQtMWG5RBSKZdoGga4AER18WOfgXBOegtGi3sw0PPy_ySQPZ9dA2sIRZ3iisMhFTZdrOfSvgrC4P8D-smj-YDkLRP5rjCmlQYKtGq34i1xHraazNfvydgHl4m4j2HgU4f4iDUxHjqkvSJk_oQAc8Wc6eFLj1Iw8gor1exTRZPM21_-DtMPMsDw22kV_tREJ0NlF9m_8BiQpCPhD_DkFvmqaxhCpOGB0NxRLL1qlmPjwfsSzMn5BL8zhLmu_OrSsfsZKeQ7EyzXK9h_wBaePhm6KbYIqnefSy1Gv629J5lp6Uicd0swZtJpEdnm_Rzoei4i2W1iMCd6sbD5QfVEuBH73ezBfVB0-UdRkEWhTXJckJ9QWhpGlN3MlJm7cfDmMnSZSy4hmO8gYppNYvWRGrWnPxCdb2LZk-LTxQqDRgJmIxhGamN0gOAjipk4pu-TDY0Mj-8KPUMy6_H4_n5GsEqKNGONmGX4Eh8xLJR4GsCu6qEnG66CCtYO2fbyIQDV_fr-nXfOKc1CqyCbqhpSRmzJXz6jueJHaxl5gLcpqoRtmjyAt_c0AFqazFjGOI8vYIaJnPePWDQT27WABHTnjTHvFonD4uVxRZE3d8DxmYlelxENAfk1AJiCAKiXvxlbJYfgavWxSC37v5x3JUDc1DV2GGGW9uVe7boC1Bs3Qk5kyRpfKnJAvdKcrVZPF-qzneGRTHJq_WkQC9ItXPxizN0hCb1ZQbtFcg4Cp62fikYYAbiYTpAD9EQFmcIHsffxbxFgvSQnpjSRA7qR8j_8vfpkQB7Qm7IFD25DBWWm50x6xB9jeK5-SAWFlmFfnHLy1az4idiJwyjkIQ5v3tltdutAU4Z_epSrUfCXvVYhSZXcLUJkiZHRb-KcrSqtBC1NHDdEo9ne36rofi4qfM020FT00FG0PmJYtpXTlZIkGY3n3XTvWUHCRBbJUez0fBlTrlVgtbTDBLuvK_nXUsJjgBScFR7ZmXHNeBDhQ8R3foyO-vQnkwSzpkOy39sIzsQcAKf2OxfRmIye15pV3c0M-bKG9TmZImvQUkvA8IetdN3JIR__LDO-e6xC2M9kqr0fPm2SPAO11Y8cjxSVVmdkVKjTlMluIKTEk29i42gjrFZox3Zgf_IzVm-At9hMzeazPIJCxRAHnlb-1v_oHzf7MNat_bBI-gOeWyGAcdl03jJ6MwrfBrTAnSXZ6QCuAo_aE_CebHS5kkg2lEQD_4GHGPCGNJBTMTa-lTS14ZtHqC_u5st9moD71QzkWo4dbmheIjb0GCiPxNfERRxX76WyfLn3XZMsJTygOk3K4c-Jg14JSELAyP3XPkYy5kcF-67a-XLNfMZrU0B25SYxgesiREUphE2tBMo8MsXe2aGOdgmoUBggb5YvcLEtBQZZrZald6UtMBkp7ASUGRadWtC2yTo1mv-_hxt29pO4PjO-GLA81Bn-Qb3vNqDgcqZNEp9q5SxzrAEoht2BZwSdKM9trSpF1jQVSbyyIDhVBaXPusRT1N7htOWx_4kJkl6-bjWGbLtg2WozNyt7v0upVBBOnbkwV-HkrK7hu-ACltYXlxSchJLiHlP_2ysnZhUmQqZ2QDfHYmf7gqg_U83qVCoapibJC6sBcvgGpPC7lB4axQ047gbZM36gcU1YN8YLQQv3bUTD9plZqzXhv5pinNhQGhxoTwQCBqYc0jq6kwJS5Mhh58GnCD08F06SAb_Yxd-9CiBo2hCjCFlfDZyu52sFV3qoFsPHoHTyYfFQioPHT2TeWbFKJJNfHtdI5gTHmp3-SDywLtaiOXt2JAgLLRcevrUOYSlQAw1abTp-A3qY0jQ5rYsKeJzAPLOSwmMy6k-WBHAmZKB9xubuwyrEfDzavBR1FRKWWYhOKVuolq5WRYpi8qPaCDu8oPKBGYAt3yUTI3tWLQwy1qAslqmlLRDGhdUDvHuSeIqbgsOFthyachxdDq7SQQPwomrcHKoMyvr8YLfT0ypcIBOxgKqlTKrvot6Vj9HSnCMu5EdJnIJ4SLL_OBWlGJuPdNwrQ-BRfqV1XoMWeTvuHCRquBoX4ZF8kwG6apKl2qPFkRPysB2Tx9QMjG7fD0O8fbd4HmASBX-dz7PBsyyzUXnUHX9VH5CV59NUULtztZ3NxSJVc_rsEL3LdFEvwNSZQdXTfCQMEtYuuNkmfG8i3KXdSx9UPZhCN9TcPObfhU36h6tXL-QtZ0U_eckngLjQGOvo2mg373o0023UYrbtJwK0oWCXSo5DzDWPXq2mwpdhjSkgtYGqOXCpfxN_EyM6JDBIsN4-8gstY7qrrtxpBvF38CcicUaC3e1E0-id06Ur_VdEtV2Ii2u96FMfqTjtyZb6r7OduTiwYzG7DFETCIj1ajVEUjBpOgSrftrljW2D8uVJXaFkchDSDogZZ95QfCN1LdtSzWMsR4udXSuBY_LhDbr2g2ixELEVQ1iZOem5CRUrenx6uTAWvfzlBQkBFc5pB4Q_H_AuezyXF97J0mA0DfkeIVoCqvni4pn5qlmtyBBPAPop0vbA7TTZzLL2WEgAYHKY1I14bzC5WdF-c6KX4o9v3dzXkGydoJJv9AfAZ4ljz4X8YIaMEDRDaR5NPZ9hN0aYjvJUxux3JmjXuqz2C7Y5fpSecJYYE3yrePYcwHMLYAUkhhcstIjUW-Eun6JviVtTfFlao__IBTefVdtKpstm8Qq_izMFv0cN8G-iWYRyZHaEzZ6PgGVuQv96KJl0yJ0ARJviYG1igmQ3xlWKTuDgAoyug2YnwguCefKRxhTck9EbGRgsJlgfbWTcBbv4A9jxCsF3OaWRbmK53Ffjawolxq-VhbMITpzYmpEFJSnj5Sa2__SN0GopTvQetyI2FBYPXCk_bQ-imRBZPwgcKLWko8dp0EKZdQ1CYvkY01puOEBS-QFN50iHhriMFCcXyAxJNTFscNu2FFzeWjWP_rH7WOv0z45FukQNzsu_zOSzmVzg618Sm5NCUxoW2FVNBuedWbeYtWNJTpheY-L_vZ5b6xUCoXJkR3duX8gJXNfWNa5lDY-bYmxudpUC_uAW0jLTHYvlGIsqF9PlHZ8rqWMx0d_F2Gc8kEVpM5aSvPjdSoVAUaExvX7et_UQe2rMHS-U2zO_Yx3mrXBvesKc2yafyObUTsUUqAEV8MIcoRp2A-dRME7NFFjVbIKT3bcO3POziYOvEo-1H0okuA8SBdei8oQ3Jp7HvlVJ0sWIs84DCvJsizFHYwRZg7sV4TiB7F1AmpzAyPVIvngeIZTKGA40FtudfZHl0RhJdWtFlyykdq2yVC0dmtlYmDv1XC6KSSR7Hzdl_890fZkgvozVsQbGupWXG4nQ7q68UAiPqr_O-GfnAZX-HduqZknWA9g3EdwjpJ0keLUO6Zxg3W3sCUfus_gFlP7QCXh6VbzAj2OT_t7hxSTo-eHVlz09vib1ouahU1au6eOWxi1Rsc2enQMiD8SXQ7jmam1I3aGsBQTSG3Z02H8D2rkcRt88eEQq17RgGR3th1dI1IWnxI7dQDkHsib7xNQCWkDcZZ8HOFXKft2JkjXwFfOrus-aye2NUTkeObWjkKkIl5LpIt_8iPcruDDhoQWrG0GJKryTL29_JA85B-Sb2ETQSm4LDpBPPf-OYheibpyf-aofNs-guR416GdzulY_fdfav2t_2-q-eEA-ntGVOEN-uoADw_ljyUDf1cSe7Ngnl_K1DkAAuruFgFsJxIudJs8sSxtUhMIrFYYHtYmZ-Kat-_iqX59IKpUjWuz7278379423EDCwYFdJjHyXPnRw1AW_XfxEn6DQT6M-u_85mLiRUvlbkXjga96efzLJBVfEG8f8I-rPvg0jHbrlsMvVQSKUoFbqGXNOJEXjKRtBFvR3cnRNGcGUDcnCggJdBRnHAyRljYANjSBD2KY30TDYZ6aBZEJn6_r3PIugAEnRrzhkcA2Us5nKx6C6Ztz-E7znTS2v_IUQ_2KLFiOBR0CAV8WhI1JvxCo2aO-Y5HLvyyBSl0Rt2vjpB6hKQl8omv-DbEcuQob-sUP8PzWBVNsiUFJICn1CKxqqTILnBaCIrxIp0gLnaHukiG7nqFNcHToSZN7kqHr9YOE7fkR_AgKF7GEMJjbWhE_K04kaH0sS5ZznH2-elUaZUcYYLT__qN-_5t8FhXv2z-F_kjmVpMJ3N9MRAZQW3s6FqRxZDfoMw1ZXJFDYexuUwyqwNjiQTtErc424zVCa3_kANQ6010jChk0ksvc68ZK-0WqjlWrSmqvPhQGRLRjGZsWfftG8dx-6J0rZkO-FChUKxkMc5JG2zkTEnuLHQuyihhUTx-TR0DUJqJMdhuI6YMZrR4rmN74Cc7oklje27WvW4S-1RfQ2um8uC_xT21tGHuVlUNphS25awmpc48y_m-uUh5koAt9zHwZEIIBeweOKyf4sExdEfuL9pYpu1ixMLKxj0-oJ2HwKRDzZbGRRtrL1S5uQbCSOHHjSxVvO1O__wNCjO3G8ELbgC9Pfib87o5o5h3rF5vCcLJoL62MPUpH0dsZpcXjBAx6T5bXRUCJXpgiCK9SfQDygl-M1VEcNPe6WZbYbtS7EnNGAdzdGdXsMNRUo6QWT92xpkq8vaV9Z0KWBP7mJUMoJKYe6i10mJvY7Nnb35DBIKPEak5t-ce8PKNDJ6PA-91GxUphqz_98LlEVWGStHyRaverVf6VZcqZbpQyu-sKq2U1B3-c1_7kCPkJ7PMpmryeEEjOl8tXPo4T4HhO1AIg_qePMxUJdXavvqdDGMPq-xp1dA_xScz15GqcL0U-_xCKwiCNZCa2b-7Q_hX2JfOAKvASNb596abj8tf6rHToCflwnzZa6CuovfU4Cv7NzNQ5-MOXdIeybg6jReKDbjilsIl63rasUEbM0MAISu4kafUTpKmUHAYsAUopSAWrBnLFNmaNiCw_RE4CVtqEESz6mo5gelkB8kvgyPbVhbDKXM4zTzrUGx4rI9N4knJ_4IdUUvj-eUttVaFWEMl9YyhyiKMsYbp4N8KU6H1darFZV_69Lo610AAhOlW1erWFyVJvOYfr7ocK-jeSLRi_g1FD6tHRbHkxVVuWDibM2O3SmrdWXaQVMeh3797c3SvBHAkvYYlhX1NHa42EOq-Pi7YLalhjhEEMQBkGCsNC2tg9DKoUyO1eWX0otFbJRw0VHZseNUk5N8kZml6A9VeFbmwaXfR36vaEW91dmMK_HiGE4bpXGcbObGb7LDtpAIuTIgKUvJmmQOQZSZ8kOO2vmCgClGN0l9_4nFLbPhjvt3S6MrNb2XTlAjzRSZ-cnh3HApAriZxR1cmm_mM5VlGzMM9nk7i9FCU-VYd6onz7Por9rp0OU-depU4gfPn2AbVeIKmBIX51cNy6q0L-g1Khj_FO2tiQcxeKtqQra_wzsyk-IBNtfWbpdy18mEQP-Ikr-Dxokeuhr5p-3AIZG6F5g1SJaCE6Hx4rfTHZPvpjk97stbkxLJRuLvWekfm6uuu-TOja-NtKHwHhTsRaQV7S6oz1Cbn_kPuOVA34G_Fyc25SqiDNDXgEIQQ0Qw1kEb0psu0tNeR_5FZ3tgZE-dP5YEZ_qhzLbkprNrd4Tmo3tZ6YLZYaJ69u4yhzZsonz1f6vMeJbvwE1Bk38ZASjE6MVLzkbSMH4-puDodUaxZiyPPZQYIOBPJVEX2WmIhI8iLkSEIFEzUBUb2Afg2fHNUHF7ROCyNcV3tXrDpcGojgLk4istw-ZPr6UbVX7xJPqBz33F8imb-qN-4tLaB7UE-KsgbIoXoK4D5vfb1fMiK_nkeCdHaFjBkbiwxYLe-bKDN3zgla-_I_QOymPEirx5r-jpRiDyr6KnH_KIgQPFCGVzwOvlk_5Da0KWRi1obXDjKcBkIdHAez4U4Gp8Tqb7NNcm87w3bheLDsEUC-TNOB0RdH2_-nkRBBImUfdxbsfzJ4KZ2aT_Mb-_6YZUyKSvnemPKgr-CEiQ1SI9p9a2JcI2Q9bNCC9I9AENfP1Lo6s0_v296BfZ3F9Rx8pyxX5xf-v8esFMX0fsZ_thRhQpSxxd1gnK0h-dNp2bReXu_TvVk-Ci0yriaV4yrrrxk_ygQuOVgmH11K5UV7FJ9axqAtOiZHQ6rPHsp81XHw40L912zzmFoyudRU95joPwchMTafhPXQYrmrQ-kACafWN-SrxOVhMpIcN3JAwiXLSSF3rUfkbGMIag0Qq4BHRK6RifT59FCcswFBhCFOQWPGXvAdb03d3kvSb8TYZJVsdejdFuznw2MuQ0_F8okLwdwAq5Afn5nBvLrdLcoF8o24BpntWLwKJj9rskGifM1QzVOaFEz6ei7VKzS7VKdjWFBO4pR98O_KLvkaenzQSNgHaxXR2IOR4KYNVfQHtHao8GMCoYH5-CQLXHL0woA_c9h42qP7t1Ydcriz9jR4NJQhPEtpXR8H5DJxN78J2NCRwaUjwB8Hd_oEqI8ua-55wUglpp0drQ4yLAZ9KPORWJ792rDE-rQwrWlyq3yEZZb0k0a6PypB6tUEviJ0JdL7G2hS-OUi-DCFWYnS7-HvSPZprDLp1eGWptXhPg7oyVrbCUIverD9bHNLlJQrkaDyAcFM5TM5QN9oUy9OaZl7cMl7B49x-PxTfW44rDmPlV2ce6idFfnH0F1_mCXU5TT2NTScnGHCNYq6DF6cIAP083Sk3YVnng5W-vKYHeJnZnIVIG21Ja__xXJCrk7EjhBaAXjbAxpLpxeMbhSHITu5RsV8q7mAVKMwhr8rcdcjanLoDSnqfT4WpK74KZ1ImK4WmwpUDuP0ZYMuijNMDJ3oQ-Jlr_Kp2E-L9yu2pzbwxLoUoeaJtnFvNU_-W3mPYpkb4_I9hzwDPomhtPyqWtKcPywne8PtmsYKEP3fAalxx_zY0leLdQRtfYn_Igx3nyZRkM9SiHvs5_nmFs4zXaE2NGVMiNKXMcy6qtJZIcTyzjjl2rTXDkdiMlqNJ5-k2rVc7--iAduqXgKSuYhxOwMHpjbZV7dSiNAWgDDKD21c5WCV5ll5silYmWFVykg1Ta5goCg71NjhC-r-VSwUtJDDLHU6UnLWaOQ77FmsA7kmvNnYekYmOOml3S1tMVRDY58ojrukVxDIeramclTw766SPJCDB7rm4o3OexfXbB_I0ZXIa5O2hxI-Az3LMEQ5twd6QQ4LnpLzZjGJEoDklDEXDn6Ld_K-71kKN4KmpBgD4ZRj2izFqP3Nen1D_v-OgDdHd97bPsEQlFkVtcIq0FuHqZLSZpDWZzlrDhXxPzJI-1AXKDuH8LY61RZzcYyNVUzd2v8RRQj3m0vaRSazCWj_J8XHtXR14yuFs-kIXxaYEafXIZQJ3fMLRRA8P8hI3XGmN11sza5Ka80VvPKUvfXitn2dtrP8o83g9rWaQ2FerLxdtsA5D9GGDf51lYzESKzIfd3Q00z-yzuMcStUTjekoxVqwuH9b4frvrogeIcxARh36145UkLwfM8DQodB8X4Yc0FoAYiFWLMpP--xGH3oX7xEhBQwoW6x7MGXkwUXPSNn1FskdEtvSBPKh8f_8hNw6yVXgYhmjr0ZjvkDrhtl8P47IRHnSTjfGXR9eaLZKUoge_Dz_p1Nd4-KoYO7_OtoPERfzo48UeNb7JD_KLO53oqJXjeeRi8iZu75YUApiZE0uVomDjRyCaN114uSkN3FTQaqoxPVZs8K_5I8Q1XisYFaD_QfRB6Z5MSnyZpwLbo1e5rm5sEK7FMJcCosNlzYPzhcoG1lF_n847kNcFQokUQDI-DJdz8KzBfs1xJO96mg1efD0R8tkDY3HD715pcfujjW1sBDFrMYpnwfDNo0kqQR1ar8QbPd3n9nV1zO0utri_pBMoHyOpBgwl0QSRPrHD4c5Nud4EuygIjNhezb38UdIs2hep_okovPQQGffxfSYYNJjRVGh1AbXmAYXoZpdZhywVEUZxzWgTnjjrGNiNpomGOO3hC-f0jaUcnMS9YCKHkMJ7sQKp0jP2OLpYnC9sLTvWavv2ZzlDMAjyw6hgrZddfcwEcy8m_su7vg1-WYP4zDgA23j5GXOLEexLTAC0Ofha7tDC5mO11ijU57FTph9p1-DWGEbzcI4ytHBQR4VFDWV9lqpwQDKUZrFtWAIU0MDerHTLNA2Dqg0fvcNS5XsKwvCgCtrpF-IlNXJ57iGbdTCo8RuvngLEETNJmgMw0PhCDl1GgbZ4OI1UVcUifYqgYciKpD1qzQtALve0TEFasVelchOQSfp9Lv_ywcgPjk6og_jaa_MJ1d4i9KfvauuwC-eFAqZWwvvpkW8rkObzJAEULolcGoIWibVbFRtn7d6jOcKEt3R5ppHQwS6mwss3yElPlB2BgHVNKyiUM9TU_QARF-muxf2n_eL2EXYzDPfXqYAdLDjxUON-ChUUIIJRAXCWvpeXwXQJ6uKuTBZptN3lJcKN0G6Whllgi38KdR4YD2cg2LSwyXmd6myqAK27eh3pA9x_57Gjy3xzz2f-DW9nYV-gaqIuEU3ScmALDbx7d6kTnp8zAnQAULsrraH5u2Q5OuYDpng3ax4ZFbhp6odIDXczXfFDpIjqitw6UmPYNIohnDXpoAJKk0edpXyKEV-uVs3xCOQLdUp7aukF6Z_8BfV1ejwZ9lTiZsO0Gg46IqsqFoblQQhnW_3fx1BR2yKKHq-4YbxLX7N46kTnFYdLD_KOFouPcgpcNO64h_jOFXW3wd1jqW42EVuKq4OD41ygkcoTdi9dLBWIEFSlpBZD8sz-zU8OvJ5sYuPOBQ6UeCsrNzLqHjgkhCSGbaOAExS1QIAKiAfMShFe9qbGJs67Ws_zvNNBhlWvp325yGT7jiJkZUrOySMNRa-9qJ8-CGZjTJKiqG-fuiH7mjNdUoS55nIqbGlVjUBONjLLxkG_DTOgTF5c4PDeTsOfnfajqjTVp0PSbl85UyC868h3szcHnb-UQXLDfg1bPQe9laADMLA7bPIPheipA72MhLrj5OO3HfHhSb1v0hvvqUb1DY3EJRcVdoa7sYT89qdgzZdESP2Aa3XvH6OPCjNJnuyygiEUxExPiO0CId1uXrr_yVTKwQtzvmAwzH-RYcioedYClrdDfC0hNp8Cx1neqFoRCcBouIhc6Am69toT1Ituv2fDuhkBPGeuW-2d01UxKLueofEJCh9nVfOVsG1CyzFzulJ5p20SeEHBPjzxUSzYhdKtaAnSq8bjrJARyIe1bJG6ozMMJjzzJks-Sp2HkTgL92CHL8DZ3bq3fDjALSqOLKWI-3jzGf-kMog-DFPpFI72YmAWke55q2zXPSB0IlYWRafddH68xOlArB59Z_jTLBP60Oh2UhI9F-w4kt3Ws-RvxfHKIUyRkelS96N8vQc7AAtAjdLwizDBGy1AgFR-exeWbaHJviyMRZ1GrpEQ3H4XJ80OUS5_zDgNozWULBWgamzN49j1ajsVNqd2X9an7xcJL06LUCLh9I0mkbtuTkhRlfnx-zkizIMVN9HJLr7fb-mWnonNFWfplAoWD_eAc7aAqJBqKYpW4YDP3fPAVtr3Mffb4o97LOFC3JmyOdPJDoUQcJt6NcJut_H_24Vf_hzp_WdDnaW6Y5GNViMom-fgvRYC-WCkzIg4e6Z8duAkFZ9pDFkthVrxt8j4hfoBSW_p7STlN75KDSWb4t_XscK5gpi9CjIV55JB8yHAuhgITF1AXXqseafoXDTPkmN97TV21fS5rQ97eadFxoaQiTFxTcY9YtAjWYph-IDQ_VJO_Ym1I2HD34K1qz6Xnu2RqcsWm7D-iqzxXO49BwGEJShYInW4TAlhknHZxxNDByUqMg3qmiNnVWBy3lutTshFLabz8ZTF-gi9jJK7x8QBsUuFc1kwScdAjLqAgFgpR0P6JP8jfITLw6n1VDBob8ko0fpxwMzjXBngZ3w9SbLZ5sRRNdLSNSezjilrG33Yh_EPNpXwiLxf8eDGPUOmaJ_YvLb26x3_bcni9yAP9wbgbEQj7kjByBGhajLN4yOdaqb5RogaXPkJ1KC0Adn_mNL6jgSB9GMirLYi7f-lBjyWVLQ6eflKUdyOzpQBNdW1PWYBQyFvZ-ZlkvWndwByUQopyt8pemuLGM8Z20tkT7NkYMBuapQI_lOCVBGaTBCBaKhgJ6580_2t-Vxef0YPZBeRwy8CmxHDiNcDHNVzkuz-J8TxFH94PBFQeDpdnwxQQ1T2ck-NYBrIG-bgNyASWDR3nxwwvXKEHVhh5XLGjY2mQXi_sO8zdSX46H2U9l3AYhLouxjuw7KRhLMSV1lMflLfm23JaNODYY58bmCIPhzFxT0FYZu5FC0uKBLl1JxTENEXMCBPhXZmGwBHLXnLgaDkiaRdSyxaopLUptGiemhVsgoGuSYMidj63dCfsWYB9KBS7EJ1YQpY9ym8g07wiq_a7toWh1SDz3oG5Yiccso3GHncGMGblh8vx8nJFN-Q44hENB-BGxMouJsHKnvqau9zVRUpc6V84er3nmxxYtfjUV7xB8j09wXiy4heU5MBDtF0GjBKzrhi7Kkvt2op5cXa5xvtAO5Joq_29w5I4p60yUfXta_HimvrkdjZdXlBlLA5yNbA7AdFywdITE4Wc7XAiJ4GOC9QyK0CkAi0SU2IVEbE-JyB7G4hnMVD5fpf37y_oIzck9abnVkeALN5Gbf-sbv5GhH3Ge3nDYFULr1zq_LuqfxDv4a7Mwoc8Gl3geYVuoRKzcUgTOqyfCVztgxy0QzMiUoY58ipFgo56tbREQvEO_NhMboMVYEZNdbWH0QdK34femht2x2ICmdkSla9J4x8ezxeXg2xbA78LQ_bOGwupykHkOGfZXHvA_Zxsyruonbvc4Sn1UKShBewSXE0rP_JkrTjSd9GGixvy_UzQGozEz4bJikWbpkMVMAocOOgCDuvrPzxquTV0_zy4oT87PJqeXMaF9sR3fQQPShsgwNbZ2jFWNGg0mYIEHGasd7pTJ9rrALMIf_X7YhrFxPBZ6dMZilCRbRLlb_sk0_CVlJBdq4xQvBOMKJJjEE67dJwpgVugBMN8Zh8y8kQfeTgYv3QOXjm2QyqwdEMpFf8ZuKEAeioHkmfa00yAnyOpjviFwM-XSWnqs2G-yRRftmcf_COhcNZ-4vlbfQ19eczuzwRYzbW-rtRWRpGwurqpcs4GZ2z9tMu4jVZR_D31JVAuVXjAOEQq6WCfUD_gh98racMYrfZ6FTcc5pyLP9fOpqmkCiaVgLlZHDlugBWNFxytFutA4uBruTlfasC4Y7XVIHmfTMSCdJd42qXGHvKFibDUgIG50seZosOs5HKLQMt8IfYtc47AIRHqSHN2e_N3vk5beErp4Q-QrX_0Okcp5NeP_q9iC1-_BYK-wAGUPr-nRxd0GT5yeZYmZQnDvPe8Xz-Io1i93gPJscf36YmD5TIhZN-4AtD_nZA_X0yf3i_hJpNKPQEvezW-XstPV0OfPaknB8btDXxfkXAAjyXICK_J6VCd0REP5i-CBOjPGQo-O8uhQxp1uv9tWtrI6VNyBqgl6VAvUKmNFjHpKL9h8rZQZAz2KKQj2EYfSWg4uTjSMeOZDNOgKT7TJZ5a-68Wl9k1YwkgPkbEQyxk-fLz-l4KRPNaeNWFGfa0j0Bwsf_dU7OWCzMSI2xBo7Mqcd_sx6Ovp1HZ0u-t8hs1MtDmVaQUPsRsbz2vBxifQqNRMlx2jHh7GmX-Q37VHFTBYGzWD9nNAvAV4GbLcXxhm_iPPzweuCml4Y7ox7k5m4lzejajYbjuYSn7Zep3eLG47qZwZhL_01ASURm10Op1_Ub2N23d1vACz9ngHwsTbjxPpPDMCTVhVNRIfBIouiiVNJBudGpUb5p4hzOVw1YQoZHTQNnjs4bxFpC0cz2Yzaa1CuQGf4TD9R4N8ICcS6dR9l2LjdNFr3bDqG8fV80WrL2XZ6VKdcKUYRtKR5aRHlidzYnahEtSQztBIQN91qcfSp9z9u9p3E3zNkSYgmGnR1qs1mpOnCT-k7zagMjDHOPoQ-i_ZygzvI3BUKrL91RV2MM6lc5L4lcNbCrybDMMzqnSYRBNJ7Hwi8nm49FEHTogr33JawcUxffIyPivUydrjaJ78sxikAEWyzRoW2fURTk0jgZu2E-Cj94Bdil2EkTWqjQE_eeTMVAIpZHYpzbP3XFxccFF2Ks40Ja_nNOgldRCKv5Tn8ZZzluzUBJkBEpZ7JwNiYIiarApPJrQlsiTBN8BXbEl7GpHeyl28iEkQGeE9segp5j_pJNLNWMvCkmfhvzhQpOp3XYMdF4lNyIHsXt6V3IY9a9Cu4-ncHiUhdx6poc1VupEa8QpReo2-i62cPUi0n3hQIDfZke81FrW7Kn8DrxfxaS0n0HkEMIdzhKUM_ZlXV0sGWfOddeTRaZ8F0Koc8CgeRB3mRtFWj4Kp0eBss3P4DHsCfI45d3WBI0ucz1uWnpppykiLMnZsg6XpgTqIbNm6EI8eKHsVzhUVDaK2RGO5LEknZNlGzV785U8-mFPamY869JOF_ZTaA2oXiO6o-zs7V-I6-AM35K4BlVaoZOySboynzazo_t3-uolUwpEIdA3FetzgYTcT2YVm1Sp1YL0-Qqvp7sHIWeYiIV2OU1UkqalwS8QTOWpy0zMNkTI0TMCjJ57VmW61FvGrzk_TDrUVMNi34SPXjaX4WJa5OMF7NCXpvXa3PUkitICb5ME_MtsqxuhoHX2-PDT8mGccmEpuVaDuzJqHf_SW-1Bq2STuEH22SZn-odPnH_6s2sewL2CblzuBs_qxSPshoySG-k1S8ITEC1_PJHSO_MCsdQcM8B0y7jw0dRtqOLT58xhG9NYbGqyaR3Q3_wpvM_ml9g3fgJfjJOq4IXVp6sxtGGyB2CM9DrIaqa5g4vd3nUkC-g2LbO5PfjUq_Q21hFZum1JwJv3lpDrJsDdoIjuzoVwPNAEtRwM8EyRD3iNcHJAvlyN_neCEYBk8VifMr-Zr35ksNhchkNiMHqZrfcuy00PVDFH19Yl2lW33Iv7vtGNXZAvGcd6v48keQUMBf_N-coop4-4kVwRhAktwXvIx4dz_zeiIfnFQwYgq9HtoOmUXPdLkP1366y5RrFAtA39fazs1gDhSlXYIC46L9AkOlOA3RvN085IRAsFldpEa-7f1W0cfh4QdECJCR8BkxhLbf1kC-TDfrRkaUm \ No newline at end of file diff --git a/.gitignore b/.gitignore index fd8c885..6c865d2 100644 --- a/.gitignore +++ b/.gitignore @@ -16,8 +16,11 @@ pg_dump_* test_output/ *.out *.log +# Migration engine runtime artifacts (relocated under backend/migration/) csv/logs/ evals/reports/ +backend/migration/build/csv/logs/ +backend/migration/evals/reports/ *_report_*.txt *_skipped_*.csv *_valid_*.csv @@ -48,6 +51,24 @@ terraform-github-repos/*.tfstate.* terraform-github-repos/crash.log .abacusai/ +# --- MEP v2 (Migration Evaluation Platform) --- +# Node / React frontend +node_modules/ +frontend/node_modules/ +frontend/build/ +frontend/dist/ +frontend/.next/ +npm-debug.log* +yarn-error.log* +# Python bytecode / virtualenvs +__pycache__/ +*.py[cod] +.env +.env.* +# CSV upload staging (keep the directory, ignore its contents) +uploads/* +!uploads/.gitkeep + # Terraform plan artifacts (binary, environment-specific, never commit) *.tfplan tfplan diff --git a/evals/datasets/tier_p/02_empty_file/input.csv b/.logs/20260714-120638-5cc98304bd6b46cd8995f7366abcb348.err similarity index 100% rename from evals/datasets/tier_p/02_empty_file/input.csv rename to .logs/20260714-120638-5cc98304bd6b46cd8995f7366abcb348.err diff --git a/.logs/20260714-120638-5cc98304bd6b46cd8995f7366abcb348.retcode b/.logs/20260714-120638-5cc98304bd6b46cd8995f7366abcb348.retcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.logs/20260714-120638-5cc98304bd6b46cd8995f7366abcb348.retcode @@ -0,0 +1 @@ +0 diff --git a/.logs/20260714-120638-76081c43c42740afab863c4d1aa7e876.err b/.logs/20260714-120638-76081c43c42740afab863c4d1aa7e876.err new file mode 100644 index 0000000..e69de29 diff --git a/.logs/20260714-120638-76081c43c42740afab863c4d1aa7e876.retcode b/.logs/20260714-120638-76081c43c42740afab863c4d1aa7e876.retcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.logs/20260714-120638-76081c43c42740afab863c4d1aa7e876.retcode @@ -0,0 +1 @@ +0 diff --git a/.logs/20260714-120638-dd029799a43140f59bf3dc3a9bbc06d1.err b/.logs/20260714-120638-dd029799a43140f59bf3dc3a9bbc06d1.err new file mode 100644 index 0000000..e69de29 diff --git a/.logs/20260714-120638-dd029799a43140f59bf3dc3a9bbc06d1.retcode b/.logs/20260714-120638-dd029799a43140f59bf3dc3a9bbc06d1.retcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.logs/20260714-120638-dd029799a43140f59bf3dc3a9bbc06d1.retcode @@ -0,0 +1 @@ +0 diff --git a/.logs/20260714-120926-043b35346de54103ae934160cb40769a.err b/.logs/20260714-120926-043b35346de54103ae934160cb40769a.err new file mode 100644 index 0000000..e69de29 diff --git a/.logs/20260714-120926-043b35346de54103ae934160cb40769a.retcode b/.logs/20260714-120926-043b35346de54103ae934160cb40769a.retcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.logs/20260714-120926-043b35346de54103ae934160cb40769a.retcode @@ -0,0 +1 @@ +0 diff --git a/.logs/20260714-120926-29affadedd99466d8c20bed323741c6d.err b/.logs/20260714-120926-29affadedd99466d8c20bed323741c6d.err new file mode 100644 index 0000000..e69de29 diff --git a/.logs/20260714-120926-29affadedd99466d8c20bed323741c6d.retcode b/.logs/20260714-120926-29affadedd99466d8c20bed323741c6d.retcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.logs/20260714-120926-29affadedd99466d8c20bed323741c6d.retcode @@ -0,0 +1 @@ +0 diff --git a/.logs/20260714-120926-b371c17d1ec94ea99f98fc35e4e2a8b6.err b/.logs/20260714-120926-b371c17d1ec94ea99f98fc35e4e2a8b6.err new file mode 100644 index 0000000..e69de29 diff --git a/.logs/20260714-120926-b371c17d1ec94ea99f98fc35e4e2a8b6.retcode b/.logs/20260714-120926-b371c17d1ec94ea99f98fc35e4e2a8b6.retcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.logs/20260714-120926-b371c17d1ec94ea99f98fc35e4e2a8b6.retcode @@ -0,0 +1 @@ +0 diff --git a/.logs/20260714-121622-3109e609c96e4d10a4efba1bc6cd4c6c.err b/.logs/20260714-121622-3109e609c96e4d10a4efba1bc6cd4c6c.err new file mode 100644 index 0000000..e69de29 diff --git a/.logs/20260714-121622-3109e609c96e4d10a4efba1bc6cd4c6c.retcode b/.logs/20260714-121622-3109e609c96e4d10a4efba1bc6cd4c6c.retcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.logs/20260714-121622-3109e609c96e4d10a4efba1bc6cd4c6c.retcode @@ -0,0 +1 @@ +0 diff --git a/.logs/20260714-121622-98fea740f407491aa80f035877571b98.err b/.logs/20260714-121622-98fea740f407491aa80f035877571b98.err new file mode 100644 index 0000000..e69de29 diff --git a/.logs/20260714-121622-98fea740f407491aa80f035877571b98.retcode b/.logs/20260714-121622-98fea740f407491aa80f035877571b98.retcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.logs/20260714-121622-98fea740f407491aa80f035877571b98.retcode @@ -0,0 +1 @@ +0 diff --git a/.logs/20260714-121623-8634b54612934d2a920e6e9ebdaa642c.err b/.logs/20260714-121623-8634b54612934d2a920e6e9ebdaa642c.err new file mode 100644 index 0000000..e69de29 diff --git a/.logs/20260714-121623-8634b54612934d2a920e6e9ebdaa642c.retcode b/.logs/20260714-121623-8634b54612934d2a920e6e9ebdaa642c.retcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.logs/20260714-121623-8634b54612934d2a920e6e9ebdaa642c.retcode @@ -0,0 +1 @@ +0 diff --git a/.logs/20260714-121623-b8972cb1fc964026bc7faf57c4a5f651.err b/.logs/20260714-121623-b8972cb1fc964026bc7faf57c4a5f651.err new file mode 100644 index 0000000..e69de29 diff --git a/.logs/20260714-121623-b8972cb1fc964026bc7faf57c4a5f651.retcode b/.logs/20260714-121623-b8972cb1fc964026bc7faf57c4a5f651.retcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.logs/20260714-121623-b8972cb1fc964026bc7faf57c4a5f651.retcode @@ -0,0 +1 @@ +0 diff --git a/README.md b/README.md index 9ae0a0a..86f96e2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,50 @@ -# T&E Database Framework +# Migration Evaluation Platform (MEP) v2 + +> **MEP** is a web application that wraps the existing CSV → PostgreSQL migration engine +> with a **React frontend** and a **FastAPI backend**, turning a powerful CLI/script-based +> tool into an interactive platform for **uploading CSV files, running migrations, and +> evaluating migration quality** (completeness, integrity, business-rule conformance, and +> reconciliation reporting). + +[![Status](https://img.shields.io/badge/MEP-v2%20(in%20development)-blueviolet)](#) +[![Backend](https://img.shields.io/badge/Backend-FastAPI-009688?logo=fastapi&logoColor=white)](#) +[![Frontend](https://img.shields.io/badge/Frontend-React-61DAFB?logo=react&logoColor=white)](#) +[![Engine](https://img.shields.io/badge/Engine-CSV%20%E2%86%92%20PostgreSQL-336791?logo=postgresql&logoColor=white)](backend/migration/) + +## What MEP Is + +The **Migration Evaluation Platform** does not replace the proven migration engine — it +**wraps** it. The original, battle-tested SQL-first framework and CSV → PostgreSQL +loader/validator pipeline now lives, unchanged, at **[`backend/migration/`](backend/migration/)**. +The FastAPI backend shells out to / orchestrates that engine rather than reimplementing it, +and a React frontend provides a UI for the full migrate-and-evaluate workflow. + +### Repository layout (MEP v2) + +``` +PostgreDataMigrationApp/ +├── backend/ +│ ├── api/ FastAPI routes (HTTP layer) +│ ├── services/ Business logic +│ ├── migration/ ← ORIGINAL migration engine (build/ evals/ infra/ tests/), preserved +│ ├── evaluation/ Migration-quality evaluation logic +│ ├── reports/ Reconciliation / evaluation report generation +│ └── database/ MEP metadata models (MigrationRun, UploadedFile, ...) +├── frontend/ React app +├── docker/ Dockerfiles / compose +├── docs/ Documentation +├── uploads/ CSV upload staging +└── .github/ CI workflows +``` + +> **Original engine:** everything that shipped before MEP — the multi-engine database +> framework, the CSV pipeline, the 23 eval scenarios, the 85 SQL assertions, and the Azure +> IaC — is preserved verbatim under **[`backend/migration/`](backend/migration/README.md)**. +> The documentation below describes that original engine. + +--- + +# T&E Database Framework (original engine — now under `backend/migration/`) > A fully parameterised, idempotent **PostgreSQL database framework** for **Test & Evaluation (T&E)** programme management — covering TEMP documents, VCRM traceability, test execution, defect reporting, and multi-environment deployment, with a built-in SQL test suite. diff --git a/backend/.abacus.donotdelete b/backend/.abacus.donotdelete new file mode 100644 index 0000000..8250b60 --- /dev/null +++ b/backend/.abacus.donotdelete @@ -0,0 +1 @@ +gAAAAABqVibVniO0KajGcO16Z2DqpjOsoz_Wwk-hlK9h55ElasbUvl4iTms8CgWxLyIWlmajRqbRNtlFNceS2bR4o1kKdK2nib9It0X3o1c34pJjYBC9oChcduhdGK_hA4mWGMRPGXtCgyc4lLTVE5RM21CmiEsjT-DWcI48t3iJFE92mzBEYfyIGG5oVaIF3pKHeBTenmEhmmj7Dp5tqNxCRk1sS9zcZdZU2Klx-vF_0VzjCLuR2zRgj8nZQyeaUpGRlg0fhKMPwvrOCEsi9CtYxB_-IVQUAPvXbOYNAs-MsVfexG2hhNiF43wNDXjm3YSLmlFno0kYPdoDOWBVPwzfRv_VfJZ2D97wAV7Br6zXzSdLw5sZ7NrAosR6Y0nFfz6qXMbS1shhPAI3tZVBTHMZenJomS8pliMo0MiaciortzmqUyr1_dw2yFXkg8XN-MGKgwvHS9EkrRQEZEQxvXzDImotMdRU576oVI7qlrIizTnndguWzjlttgVdX74XiGnHJP8ViddRZ8D9qp1_CbY5oNXUXRWrZaoNvN1HHOnSw7eg8JWZFIXmxuNsvWEFnjA4NRqYWwK3vHfmtFvSHi070JFAXCquFlaTnz6Wk4YwlfAvnsjiM5-I4lCWe30ze01Q3oS2xjPWVQExROIuPQhmHBDgSl-HFj5w_9KXRtf_AU_cxzYRYIYOsyr1t5Z74FBaSoDBjtRFX1FLu53xifRSLAz0b8tW-ZQwOK9T03pYQGd0-R73-P8-GUVrz3wJBPf2sfxEXbYrpOEDLOPBqIeA0iTaSLB3jm0ptMYgLOs06kdbFok8CZ4sinqkpk8vAqYO0BAAZ8CPGnt-30PLnf-6iRHmwMHoKie1QZKp2xssV_mRyZbUvJOd9_UJP1hmp-D9b2le72tiYJXIBdNy7cMfmt1FLodxK3KX3Hk3wkGUwPJjkyJJKxIYaH7GnJwQ5mA-IhufjfCpzGRIUpX0-UBQtJVfJrr5xWaJNO0DTdMXDmBipmA7DthHMyXIk4IxhOZi4M7huhnlS68J-5D8h1QL6DGSrhqOAmyMxd623qC224cpalNI7Y-fDm9eO2_IN3QwPL3M7ba087T2k29VY05B9RxHHaJKV5sflZ9nJzIV8XE8LyCtn2p-_o_I6YOUlSoZeFZmSB9g3ulCV4s0hGrL6xqftdL0Dq03cHkrS_xGWQqiMHXDtmrhigq3OelNv-QTLe46nMhqviE-1oUUk9uetVnF2bzJA-DwT-mQMjxr0MyuNT69NSja-xt61KuIEdW0aA4pBr5McCmyQ1Nr-3FH2XAgW82CSFydVlnB2pXnbYM1kdAtLzHVMpTyoXV2GT9j2zHbsuyIXiULpsekGnDgePTxBiqTmlD9pwUFbzZ4zemRz6SAmZRVIXI_T4UDXUeaGa9bK2-Fmcwl4ZtJJRLmbINpNBVF4FYX9GYE3vGC34XIzB67u2O2_tIaii_CzG_oG6tjIqepoD79ArwaILOy-6EgRXcWhmbaZnn7vbogz-ayHhE9QB_anAfDZ0cXbdyyd8-T36SUkpM4GNwkejtt4VfkI6kmivN3PDfZN5N59n-S6eqOI8p7aiwo6DOkDZ4h4OBM02wVF1MeIQ63wnV3kwDI-UdJ90WKoJEFQPdFOIegGHjUITS8Zt3sWEMp59GmEO_ekIbtHtvhSKGg5PzuyJF0_CXKhfW1H92p5kvpeH8eSbqBvrGGR89F-uOhwbq17rSA_m5tNcY3L53xGXREnyeUo9iBP19hvNZPYdqYhDRzwJvjACqNhMEzSjIklgW15QFyAs-Xh9phCGdmFvLQaapmTSFmA6--cD0w6BrmoEVyZ91lYhthVf_-3QVbPEdGNyEmWkw4Oi1r6fkwdIFh7DHZQwua8WeHFMwqklxBuWxV-VGs-FdAPbO850MeSBW3pyJXSOYcDkEk2VHXbMnP5E8Z25_QCKk-jv_liGG13vEMgMZ0U7lqbJCTFKnTDooA2MM8NE_Nxfy7phHk17RbeAksst0eMHvTaU33399S8FRC8U3wMGG_rGl8TA2QAaRk4NOCuthcrO7yMXyoxQbRph8C24tDviTT8c21dnbdvZAJOAXXDjEWvk1SzlfVHe5S7rc0jo9i1b0R5Ijia4e3M7ZMdrCrDpEMA0oUj8Q6pDEaEnCe-AjrQuUGB1vpxxZRSWzJyMwHZ0WKq5xzm6RQZMeNI7_nENDWpbTAAOZIY4gDRlRGBbI5VPYSEfxQSPG8egmwwB2odnTUJUi28y_72al1bdiNOMCcTuqp8LBwZwGkRJYeHjZG3sShIDA_yeeROgQShg35sjbrOGc5pdZ3USZ4xJY66HKSMTOw2cdZDilUJpBscsat4SCajjUekZBybZZLHGHnSW4pJInhr2Vj6sySH1GtnAQHFwmmsB0tqmD37AldEAGRwiQOrKHyo7ueARipVYz_6PkEMjp1VkVGCPehakW3atQ_yApqPzvEm_OlxWdBTiB1sVc_paEteobEuMQrSvImpJBMjRlxJXkuMDl6axw9Sjy0viXrYBi70PdEayQm0SIDQSOvyMq78gMEHDVPF7XIkZSPMgfBPFnBxpFBjphkxcROqAE97pCjqnwvtjYzEz4pWauzy1A4URXnsdRN3zVsBXflqm0vgYUMxX7OsMg8-Eh9fPQ4tVPupO-l3ERWV-NoEgCmslQQ2g78g1n6cECq4vpdnFebo1oZFAv71OH59W2u6xcUi_a5WZkmD9Fumgg4fOlXvc7hzUBdxfbxsOEW3feg2N2Mv1a3DqNyhtq_B_pitJ4zWFYH7VWyVAYHyeazmbnSEbHhg0hG2uHOWAiBKAET5jcESjq9ONK41Dm4PuTx-JICq9WHP7Iumyx3qR8YMePDsxFmCPPlWq7KOfPawx7HALfg9cj3woERz6CgDAzqH01Zmbsn6tATMxiQFEcImkm1ymCKMvULvs8OylQAK6ypxBsSkSYViMvIqmwFdIUMMKv_-9DXdxts55tbrUb--owC5td0ynfeIscUFqTmT2Mcwo8xvO1kme8MrQevKoXJy1Ohx6vrAGIl6QqU4Xm-UAF7L2eJh6iYQCtAMOJn6caAr5bCxOdmSfzcFjLMW6udjdh7O53FO7f2V6gHhjMi-mfGS5j5mksvx_luKlxpiUhm_lsiHuSE5ZlKqjIpffWT3l4LeTlp8-Xw2OIJQzM6hYMdjiEx7TGja9kpLsefHp30UH6Eg4aO0AukIynTHOGP5HVW8SU7JIRZWuejkIfdcFSoDjCj7SsTJzk67PcwzPZkhi4kjLAwnDv_w-6LfMlzwcyYn7O8pbIAL63kdfJomI-jScN1ySRE9a1J0pBt6BUMvFLLwhkNxGTzamM-BwUOTQ2KXIjrk08lw-ct4pdy9amtFC_Olsj7FmCS5zuGzQXvMniH1is5co7LwnmEPlw3ecy6ASj0-QZW_83JR7Z8_K6RV4KykGt71BavEfIbY5KJJgiia9bwhAkcPR3ZtRKEHg6uLrBXPG60i3_yziKN0miyLq9sMkM94si8PmYuWNEF_496ajwZKazQ6oT13TWayj4O85JnJDOBV0PQXXNiU0flovBWY5w0SKPWbR3f3r1fdsepqAflakjRP9224dMK8CGVaE2bHWnTuPheIk8Fd-go2wbBCHYDKo7G_Hpyib3p2CxCjhKHzbEB0IuuSU4U4OtVauGoOL_0mKP-Pn8DyIRBNoFaOuL8wWbKaTsPRz-S8vkreeJx512aXXE6UNhIHgQ4GkekG1vy0u-0pioR2BtkXz1vIkEOxEmbchKoxdtoeahNshGFMQW6qOryJbgVg5VTsKAIUe_Fa0LGzEmut1_R9_932JKGGho3tajE8pgBR78_gPF7ntMT9tbClgECxz5KzJ_GCu_RLJb6xzVrLFMVTubmESNBWtz2uU6Zd3-t_jPsdqv_ehM2uBgnYdZc7RykXnNDkBCT38NfEzUi-2Rso6OO2wonUBf-bZ_Vx9N6fxnrohCNAqFenhT76KHOTYHQJyLTFUlGaxdFlobhq4upkN-G6TC2oaSc2rq5NmtDdy0CKIoz6Uc4dESJestLgVpNEI-TN0JUEBJlheBeCZBFFOnah4j_Bt6vfmBbzZLiX9U7LUjS_ERBGCqDRds8rNW5u7y31TmVyskP_rAbIrTQp8oRkfm1pj9rjJGzZqr8xGsmZoqlXMmNzTRrj2nmPevBh2Hg6S69HX0nDIxaHzZzfmh7t-_OuGEqD-VMg3fG9BLIVdmYDZQgsfYopolcDQPNUvDoffkIzSZoncmyOnqq1x9iepJKRMXp-ctv7kl1ADxIzh953IGUbv5SrN3W8Pr5U5thnmq3atRUKgByGiqCEVprwLy_Yr_cVnDrgKpc81f1drCKFSHNcy3dOsIQF57HaG4NV7F7kO3_N_z5xG90CvLRgSshKzGFgHi0nHSOm9zzjrbamw5njXWeBit7FgJnpyWyaRy4t7PqIJdIKBNUO7Lw03Wtu8mpATtrxWoaOVmC8J53v5imO8quHd1RlK8CRbkHPWcMbFA71mLY_oIFb-_AQK7lmM2L1b5FUa8fDTki5htzGFdhofnRg-AHHiIR0tKPJ5W5oMLHUfON52mokUhMXZ8sN7QSeOwkfrNeVF15_CX8cVJ7qqC1_jxWG0EceYTukaKx8WfzfAP6iEKklGSxasVo2WYHtkSzVgzBlZo7fpCWUkG9rqDmKgFzE7wwz8S1B1iRfj67Ja3e14Wc3alcmiyT3odw3f2L5tfojonLuLoQvRcCoKuf3qDDgk-tw1KVpJS8VyDlrOHKHfAya__4mWqXNIOtjoVGCzoI1WyUgDu7Cxn21uR4bBH5JPo3fBZ1NcfEbHfEJhxfqYo2sdpw1-_VFZPfBTjczZpmO62JxS6maJaUekSGLA0wqtFuE0vRocV0g9JEPYd8OSN27c94gBfgKxgDPrF5yRWVSmVZJyn5TLZQgIUNtqwqmI1Lo5U7-iwEMwhvjYCr61zmCkS2HKnB8AFHh6kNEUUftqM9LbJj2BmEcy9xmPyETN7JNr77fFVDlQmgFFZaIlKa3Y09kHegx08HgKufhCx1fENa-U6bKonAW_-O2tGiUsGuTQmYzLGxSYyGLXoPT6mC1TMAoq5POJxE_u5K8tEd1KiVKJ6c9-iISTCTxNRqgLeLk6OBQo3v_x_xNnjgiCB4Gm5OwfOGd9sljfIpnMq0DbRJremGsF2MW8igbYoHO_NryX6irr13R8kxQL1PF0LQc__A0fYLom32FKnK6y98WRX6A23dkHMMiDtpngNOodGwvORQtp9YNLp6oRZ_3WpdY9v6tmuPqEDPWB7SsZ_RskuzLPK2MMkDb8BhTsVYS2LbjeWHpaVAWD-rAo3ZUQdkWaHQ1q3LECcIEBLPn2dx0cKOnDrj2Fee-kmhCKxKWFkrWFDLsiwZWwsZ5c5lxufsJmOzFihGcPkvfKQ6Yxlu2K_RF-Ya3jSJ14pKnzn2OM5GiWMxjTuE541Xd0opX9KAbMZq3rgaCVHNM7tmCOJmkKXtkwSBTYL9znHhCCDcQ1mXQ5FUGaWR_hR7KWr5P97V5tOAFv01yt8InRLt2h2jY_oX3rcUpVp4MLP-UXsWAHmUJ2uX63IXFBXu58hq5Li7puqFpLyLUh-2NpVGaYGTGzSjw6FVMxqEhk3l2lYVLc08SBks4hDxV0x8U9r0l7LsxETp0Yn43sL-ijP0rjGqasLF3v3U8Xv8rYwbh9lEnrfl31e0z-gaiTOUAW138x15zG0YBO8ZjUcQR5jSY4uaeV3kv02KqD4GhE1aI4vTsKjduWaGZWo75sa2CA0zNDKpMyBah07I0bszrsqr3qj7g09POzGXfZjeOHaCbdG80GwF5INfCZsDrX3dpES8CkYNXKfDvTJNoFvmTEPXdw8Qk-QEpFeQ-gmmrpAExO5mpTxFHtdnGMnKLaVBm3IHitWxYwuPjIIMrNgq3MyG4a7f_rdH7b90qwzInD69w3VFwY8Fh6kPKsY_goFJL5_0iL_dXta7x94Gw5W_seMxCEKHt32of2IbtgpfUtRQfw1jCyg_Ep_z-7ECT7mmiGfvRo1XBORFsQo-wh8K0M8ZfPFeFbsXNSrt3LhnKFJVHc07fuarzF79gOC3bo8dd8KGenIKUf0JYW9gcdB7P8MJy6bsiFMYLbJ1mwGYeWyWTolJJwsTvmGVaTGPWqEFStk8BLmdzLI4Js1bCMBzRjAUtJX3t9cw0HkDvDJfjxUZLhghzK1LdKeYnw0BfDmN-ICiMnQVDF8r3eXCcZhd6uo5JR4c4gY0ppgHvQvxm6sB5so4YwnAlRMn0sTG8FDl6Bc7ZBO5ghMFo2Byr_UjkU_W86_JWZopRu3YTcBSMRIdYXS5cgHHSyLJYlycr29QSsslbCSYtCHRAT6AVaetMFxke11aeC80MrCFj70BW8ItRKcA5HxCEEGpNjRJPGuyI09gXepjAZElZTEsm5GgDNdsVEZaipbnGFgJyRBWjVlOGsRaiQcRr8KkDGZp0Y7S8_rD1QBbhyaBWv4tTxb3F1OkqGj4LBXgq-Tdz6HdeSU0Z3wAEErWG4R9v-FAPjh69BqwGTo_mXlM2_qsUlyhGo6Teob8NN3a3cRD4YTz1xZN5mMmTDc4z9DlzqeFOnlG-WLeGt9I1iE8v59BXH8Rj9GjgBEcj8xAuiG00_CrN0c7asnLccftt5bUfAF8hKC_IgE4Vw-8q3dhAXtJbWQYE_jW2DQ6KhxwNLfxDbODFG2ID0cdmPqZkk_Wc3131bByE0m0XghRCwLwpxHFfgRM-iSp9MxTVrhIyeUmivnKU-DnT2Co7E0q76GlZOCYahy9ASjIxF8t8kl0Noc8OVBK-brwQubTAmQHjlVh25eYTgWW9H8X9s22owASK_1JEt9TEFs-Fd7xD-GYt20POsI-Y2AZhl1f7ZqG0kFSuiAA_NutGR0ogTKN0JjSHj17o1vUkYv3XS6FqXX_xYzF4yuq0l5krhvhfl1mE2XlZBocoviW9s2ROyBva_8ht6PP-AAsqVYO3t84EPtKXmTLwmxSP2t_-C6ozo678Nvc1MuyQ5aow0Mso1MUF5_ucMid4pf7QE3QE_v5NuEgB7tarsp9_4IEtSZhRCUS4euqXs93EK4BW10NE4nMpuNT0fd-bOpK77nLs5QaUB2Z-EsiivTjtLwDPftvI4yCPVXkBupP-yNk0F52gWWRHqAILkkbHa4iszkTNDCjZRgmS23SfeGz4ZO0uKaJJmYmW8BxUBX0FJ4ZjN8nt2DUOAWoA5iHh_sO1SD_EBNS1grT0w147I1y1iyZ2pqtCSEdawSbypgkVItvR1k3UoTXzHJ3Fp5eX_K26EuC0PXn-czsa-XiSVdLAncz3LiORq15oSERRU2aTpP0XV-pzyi4cLpHyorMm--dRhdq2F27_sGJ7aEVQU-6CMmI-iAfNK32dNVYDdUFORLD1UNJe_vWbDHvwQF4re0qhcs0Nqh-oxBx_PsBaAeI5ehnurnTT8fGwoVDeXB5gWwwavQW_E3F5COiJnWiobIw46KsiH_g-L2xJp3XTt61-llbULUr5BwzJ7SvFPsM3-KQlRneBOaJVbN69_NFb_ebmJchg8akyLIWbf5KjxcEIKeEl_EKYs3hgJh_PGJRRuZRtJG7zrSX90I7gyxse32vVq2SNH1d7eze4yAzSs4EuelN4NPAuVJyx7McsvIXmd4xiLByZBO8MP8JUyQ-Mf3hEPDHcZpcjHZeSjBSXYak-laFPEt3797cAWI7fThvM80FLA96GSPbAuhJbDhZbofUqXbKbCJFZcdgkLqmCziWGGdJD1RqymLLQ1fOzTFC-DFKsqp7Vl_7nw64a2npnRuS2nauW-zy33CahKiOfM68eMwyJ4-nddPXKycJn_BJJS-cVHIKVGJpbnzz2H8osiPp7vNX-3iskwefo8EltzpmMTMMnCE9YPIbr1ISsrG_Y8cAXxo2lxgckZsNrI5tIBt5XuqilyOnndqFmfILXNJQSVs_K2OffqygOfiCFR_NWcNg7BF2NNxPZjvof_yWwZlVgfw2jI1ikLLfTOGLaIyYTTwX7HtqlpD2205I-nBWVH9ciRC3MijHmn4TqyAnwhQOfjKDXMEGnO8q-2JJ3gHaBkmdL558WIC5q3kPvzg7V6VBJ39s68fQv-l6sHf0B5st4WgFC1p9-iyrLN5GEc3FfKg7O5cZBmPLAsVuBLQQsjjqR43Fl2VGYllamhJ0mdR_sS1CPEeiNJdYfZ2ZELWg5PDAuWGyLxjzm6ZwTf9pbmhruOKi_reHtwPNLp22BVmTEu5ytIqIfJN9m9hNmMOdpLSlK69kfejK9NAomWR4UXrZXqDkdzuDWtlWuSY73xdOvIqK_GQHFMhJeVjfrDT-FeAYUTKfA_nLpvyZ0XSaPR3RURM6pJneUahxCx_gcUyqghPoedJyBEYEAaeK-bkeBlc5Zb1XNVfcTRn4Jo5rs_OfH-ep2U8FiboeIz98EpQEFrN6u5NO6vZqQVmQsmOdyilJuJkSYhaIMZuHvQgYp04dvBPbrmUt5RsjU3YOUkP-slE9Awz4942UEoxG2Ds5ux1QFt6QrgjJv7JUjP4e028DwaZvLGWAZJGPibAvCsQYXdvdblq64ZrQNQlrPaATuVXhreDLPMANzCzuJSBeb0rIzNdOzgjsmPriAp2v9oSoafCZL-aaDpVCaHCFFgIGivcCUX6mUnJbLKna7GpJSAME9YmoOcs1OZeG42i_Ueih00r04VSJCsJ54fG2O5dptn7u6Vd90-QSAdBk-865UNSwDen_XNu_i1Rf77qAr7Wvx9bCCNsr2Sh_mbJR2csP_Y-Kar9ZzBRCELqbRKHx2x8VjTHmcA0fGv7jlNxm1TZ1URftgYUiz-10VejboDCL2q1D6GnOnmN41t2CAvAUoPSL1_mu9-YfwGlvHJxBw6TmSXXY-dbKXTq19t-uVtVTJXfRQDQrJKQYMjZ8M8ziNtluH9o7zi0KvjZjQUHkD-xn2o4833xp8lLiaECGKaoh6W1dkU65jIqXsq-lczwTitKEl02nMdoB6qPdDVCAovzVFgXZmehfiX3UR4dXGvXutXG2HQPSbpB-yHMZg41ty-Qh_pA9irNGv0K5L-2b-nklcLjbNjcYCtTJAIMcmsDRBGqeIiwGfLu0NQLqbH6SHSmi2cqDcVsIzAI1ipAbG6sKBdqBqMxB4kC7acFCTEB9fPMqOZWBu_auc11TV_sLSXvwhUpdv4OjfWhZ3O21qmNIuyqR7VjNT_KoJq3I4LkuLDFeXUfquWJ8mLYLahcxh4WiihYl-vskVA44HoIHrxoK4NgMUBENJ7w0AW9eKcPSmznyjlJ0mRiQ5_eXHRC9LU2oA_hgYX0RClTEL8PhgRoL0jUsv0P5IUkbIl3DS_GF7qj4YmZZ4zKfYdUHj7cLxaMMi7zoyxe80M63uwy0eC-2QgrOiV5YER1SM-Es0X-J43bdFPkZPRFkB5eGe4l1SRIrLK3wlAiMW-xEg2CbLaQsMYNSEeBQwltbFFkGhww93efusX0VTMcRNA0EBYcbdnthoMD2XsVR0dKFdxcjk9Lqremoy5Di0isn9Gitlq629DnmAyh7UJmUiHjQQxaHPHYTz3zd32KNT3e3KhzPcVK6cjnaeYwLZSy7SwfxpJS6DdOVCT2F1XzX-pO0FjCgTuwpH_bAvAyup80V2p0DCZKQ_b04jY31pEvm7zsnIibKYOvs4aH6x1D9Z69dQc5uLxw20N8dOxtJI89vMoV3__ClyvJ0u1gZfdCJ2vIZwDnwgEixFgByAxFIe_Ph_L0K7BtGvcgjnoxZYHUHNix-Zf0mbfI_WdmV_CkIL-U2DU-De4hfOGvWNfRAFblwQfd2PFnh5nQrpAZ6F3A78m7RJdFjXs6ZpeZdWX_sA2eobDXrZHQtMDW7HZHe0vkO-3LHGC6CM4NcsKOIwXsdXKCDnBOj7UJfp24Un6N_ITlV0Yuxm67-xDcynvcqrYkqoodlLAYJd0hbJro29dVjpZLpn10M7TTErZz0tCUm8b_l99sApe2Lst-a-OARY9-qfydqv7Inpr_0oN4wDC__taENGRWi3BbmnSyXyuY059MuVno_dLNC9tK3Ilz4rChJTfxFcaPswN4wxcFaj-dShOFDLcu_rrNDOL2MlPMWBdox0o4fJCusMQxR1Up5LcCOcYbym7k-G10YxGalIr_az91P_ULuvuQN2RF1Sjr5fNkORBiF0sJ8pvD5E-nTiRs4uAo3Uf0WkOm8zQ-cso-nELx2dB7aufYolPFrgog93IW64KuOEu4V4pqU8QcetmKZqchLyr35PgR6u0lWruaejqloSaXGqAxR7BDvk2pvRN-WlGcx1j3Ko6c6cXiPPIpPJVInay35xUpdnkpiJrT44ukyIEWN_LaI3uZ63HtJLZF_BTaWD4hmGsn9iO44wBWuykH2nd8a6O0dc4RaE0vYitF7f6ai_eFkVMg8waEwcEvdc-Ema5Q2FeapGUmMTin6l_qHVOxPx8GSzNonn5gqxfIUzV6XlR5FRnbEjSG1l6r-1VvI6ox8Qg9dQDegZLSWPcnJzApt2AodtJXYAllbZ5n-EPJPb-auup9LVzPMwj23lTzufQTHhAJoExdVApdi8V5KcNkusug37WVQlucdIHBLQoKZF3tZhcB9k-H3zuEN8nP6qZV0M7La0JD2owNh5pVYRplXIDbFrApzwpoqUp8QIMbEy9WbiEXyetdpW0zoPnxvhE0xLzdOw5HN2FRB7-G-eFxpr3X4K5NwrO9PbkhafqWISG9K4s9m93Y9yjnhmLg6fvNZ7JZJ90gIJdKZkxFIY25_X9BXJUr_ReaBl602WoRvrD8y2vlDXNv97qvZnH7njP7u6MDsSODunUzTN9ZPSMTrENMzjPBH7oYmdiDkd-wajA2xl7738DRC59c-eY_ZJk9fow6Z3N5h19jpPBMF4yEyhICSyiPoEf9cN4NOQju9AyONkqeo0SVDNN8TML5Frmq3ilhl6ESuOxGMx0kPXXe5kHi_RlLEn7K8rqpEK7DMmT6--oDJr2Eur5iZwWkbnQgclje52P8OOZuoXoyYjAak4qwV9fAkpddUNDnKqPKBqggLQ7BXEyPrnUGBj2a92SaVZF9ZYlBLF1qmrzm8gAq6UGXXdI3Gf1w8G7Eo_OnoU07RwMDGmsPdQv8V1tAVvNyLa8RX_RWzFmXzHbJo6k1b7JSzfrUf48iP0yZLoNVcfxdF2p2C2x5fAaZcJfFJiOqIQW2kFGpoA1A43UlUym8lxuAq6zFu4qPq0HpSfogJFftLmdvwprbfB84X3Xj0Ve6ASUoBydVCidmGrTW4zZmluzgynL01wb9ZEPMpvPxqg0hzW-Y4Aqzf6vB_2xi0C0pWP98kU-0Va79pwluvzmUwDTEEmOJr6zFz6tjXKMQCRV1oXBx2xdqH9HNJhU2cpHz5rTxrNxuZLxdVHny0zIk8XPQ7fhrcyMRGc6QqEwmq94yfqT1KXfhWb5UIH-F_JpIE71OnihMjguMXq6ml1on6A9R7rqh4nFb9hH9tBLJ67ijAFSaSLHtCkYXl1wv1KaKT9LilEsvIJp87kNl7MWW4IF2gSiuhKXGqR24P-hDaBMnPu_5PUmrTKjEcilJpGs8ovtf_wdOxEQsaTxgWMzm7Yuqo4VS5L1daJCSAaiMFRumvtWDu23Hr5jS090PGLyLTkK9IuqsMuxlEwxppDEzbdczc_OQU7cHmNqXKgyQaUk_qvqEPF0av0ixoL8m-CKSEs65jpc-a_L9xhVbMXcyh2wJY4G_awOsJcNrQwwlFYHwW_89yoZMx-4BhLfTfj-qMVgNXoU6wteP_ywRv4Peg9S-GcdYtfQoRPeiDm1u32Zd7ROgZn2fkihnC2MnZdBEy31ST3LBMhVq8da_w38UfIZT4lZS2yZy8J0f5SqHYkCMg90_5AKoNSrwoDbKzEdwAzfsXw9IrBbYqWbHRq6gmkb_n-P7ag0zqE1CCRIzunCl7YDx9mxRUX43TAAAeabQtXgnHl5HS1h74_4fHvvwHsZYwUxwqPpy6GK8HLZbW117rrxTN2BMagmL4WJJH0UFcGVk5PVC34Rcc8rzLUkVVeRx6quiT_iyxkUt6aAUg3egHJS7x73sB6kYhHHcdh5cXTe05QaJT7mkxB2gXwPV3u8DRqHzhcKNMpVQ8uRExfCqduds4QpCW9n4naBrGDBQ1io4loBPUqrif-8M4BYfsfyxtXPO1Fl2a-7gUUsT8pK6-pRPRwBChweKjd8cqfw2e08ewnpPG93OLorFkR6LqRO4gp-fKT5_s9XOClTBiqXKvnVMtDi3NikVaJvjt5Bl7JA28bzwtjcM1vwXfSiiq7smkdVmXgBdDiRDVideLLmDFL2Y3l_bU37rUfas3G0Q_SnbFijXVAdUByZLiGPO9cxbfWN0uXVaRy4MmB3XY5MiqC12jHNDlkVDfgPkSa0ONj1YUcUSu8yvi5eil5pjFrXN8llcuhAI9A-6g4ZM8GnJcte5qCAgSQxfHlISU9b7UeLemwu6E8kqM7jNsH-APkuqA3REJU2XJR9LvNbYZ1-JZ0aKtu-WScmJmffZl6ur8t0Lf_MmYAMPvm44q7oUdwLHH55UGHytjCdhYwspjM-6FfQ7Y5GcSdNGGhiWVB7tz7_9C6bdTdM7dnZzFfq_OAIaCS_RRXe_dSSdnAYQQbLltCil3P10GZgc4V5J7tKJ-rmlNO7QT0kPcj6N1S50EeFTEoYk1-8aQmsBR0u0QFdDEIxIO7BEQTs9lVgZnComaqRZF4x0NVVeEkL5eHeBdN_Vz5MvOSHScFhk6Z4XTatxWBlM7Huv8Yd0b59gbCqBJDmTkUBgGAoKUN2NrM_3E-wJbisBZ-vDH64c4WNGCCFp0Vy8h_n-J5Fa-cI_bVNa1eu1gutFw-HLvnxR3JzD1_vClIFyq-4VhKgM6TeyN_grpa-IzHoSZpjdOHQtpxCBH4ipeEUNIoPk8nFi0XqpSYinLKNhfLKodTrAFgEBlL39YH4_j8eFjmnEAMNpenTuPKFkX9qSvvnl7BEx9O_3QHnU5CWXyKHsJHDpsgKAgT5uyDgJDI2OSooiAAHTHNQv2mIzFgcPdqWLkf6tyIjtikCO5CTAEOPgyBdPqy88EBEevyMHI1fdlGLGEzDe6SrSnKl33LpBj-GGQ097WNi7eOYtcue9VAKT66s8u7wrMD2d7YmhUzL-NEhM0YrEtFwEHXPaK5yzRODc5PDOKkoWzRWT3XXWifCfhnwed7psgXAehmjhX-Ioi_ziJAlznz0a-yC-XFPDxbD1KBj58kEromHzOa_f-INa3x_q_Ld4yQkEp_lAylyhL68ShTMwe7ErCdvN6pNcr6oUSh8xfpIa7lJL9lCeXlTN4xE57PFZJPNphqEvYEN3YWiTOy8a23dakQiG0UzX_fnq9yQ5KJX-yCcHFs2ypaJ0o884FxMaLHWGtJIjX19Fl08NuulJz3p4fxsX6kzS8TLILiRSmoaycPeaESKKki3BWU0nXeQPAvUOE-I62qZYMwOyXjSrB7Z4eu7YSSwKxvR_Si9aq-JXS3nVn4f9DhIDVkbCiaKWaqhLJxdae-Azk5vwcjqLvDNDj3P2ZxaGH4QGLSM4fg-9MwTc2UbdWUjleM1UfquY61Up1fl2x8bEC3OjfjIXA_GoqvnaKbdfOGDbTlM3w-mKO92aELRwxlSlq-lf6PfaGtMa56Nb3C0aic9NHjmA3yQAF4PdDGDiulq1wolRUwyBzgq99sj85e9_MRcZ3KGzmA9ZwLNd1DNqB4Yfk44sbTHlfTvFRSWSsMQO2iZ1ZWbp6XuKB3hUryqxsfFwohePuWxFrFqHOTYyReEBJSmZzbaVhTvnaf2jHc8WJtV65mzqOh4d6U8aGkFhO6zs0n_0QGDcHziLrVvQpghY_AZ6xgEXwFsDZlznAsclZIcxYq0uF9AYvHaAlfuFDMhfCa68SmBvGUDH2a2hMto9MzOaL8gaV7yxvqycLhoVKHDzIWPaxjiQBnv-tVVtQGUVdoDNAL7XU0aKbuH1x1gnYs_SKkC-aA-gKL2On9jhj5Bmhe_PHSvFOGnAT7VffgIovGjceWnzdZRgw4tW0UWzjxZzJfpHi47QvN3pu5bK3ZW9ANFXCTk5IpUV_S44mYb_35hNNKemO35c82AwKkBuM8oxmXpA5MRnMqql7pkm99JpM23tgYMs9DWSzRcJja1nBAn6j1EuQfh3yS-DMhNmE8PehF6JANJrU6IdluPHPmDd_KDaPPfDnLJYNl1kGoVt36vbm1XlXB4Lzo7FJKLhvnUZNK9YIRz5ggG4luN5oXhaOL74cdWUbmNIHJLk-BYMYnWKXMpcoocj-AfXBxHjCWcIxf_ZPR0I_DWUTqEuoo0I7eEsaSWD6rbHX0foGNPp4h24oDiJM1OzS6vx7VRkPc0J6jPiKepAxYybbb5hGs3YAfqD7jdX6b0FLeo2TWoh6nZfEozY8gExX4VUeomZ_WkYFwAgaBhNjAibLK2KDp-pya6usI27Ew579BwscPOjxFJzF9wcAFY6TtVg4YtYgfzXXldEdeTFet9kLFHiyL40KJqwXN2MTBL5yCi5mtqYui2tKAXlO12JcM6RpZ3tnDJyPRic8BzV3wzvr5MR8klHNysSBzIrt4p4nMD_OQQ_P5ouw50wtmsqdOXCOFq4S7bJiGTSOV4tTCK23kx0FXJQ9cqPCKPo_SvVVPV8hRzYGFTjWUzD-HBa4LgkEQm4z2EiVUBkg8A1DxWjipHmZZDY49ZoAawW9er62nd8TghJjb3ckEdtbEcVwlID1up71YS0tL8KSN6MRPzVWNuzu3PK0PSyXPPBYjIhu1aoJM6r6oZr0dYx2WMl2CRJnjajpL7wosg_-VdUeYkDDIdH6CATX8c4ZLGWV9ig1FmUuOzt9iobTZjdS86p8xx4DFwxaV9EqB0TfzZxgMbSNreOQ3XyhxPKZPc_8scyG7AD9i-fnWel4QnMu55CIPcjJb4CDAPlfYvnfR9vyV5AUwE88KrI3Q15ApcybnI1x68iz8I__1qV3DKHQ1dDlqFvKOLw3WxqMKiwnFmt-oPV4I5PyNjfqiUksx6L0wbqII9njnlITTL1lSNn_WmsULqyRLY5JnY0KzVmbWna2Ces07vIhsKi6zJRx6fNkgOI5FAsftncJPK0bgNAlkjPr2tIrIxPIpf22rdtFN46NfC8WqGMm9gwBorBEPUsFENfxsYdxLLF2rVmFEHcfPVPzOhTpCxnj6XsISX6zAbJmGBEsfS2Kpiz4dQ1WQwTWgrZHR3JyAG_f3sY2oxcBiPit_119ZINW4XzayK0pXM_Q1ZcJvcUeUNN8vwbu_tehdRT3Ng_rU18uqokNiRuGGyV1RbyJJ89ocGDihDgVkHtkNDB_fJE4ii7AzNGNWFJXZ1z5aiG5tDioYUupk97AkrIQstAgqf3UlWOlliykJzabr9GzRWeDAcETj1lc1iFRNW0bGSEcd0kZN_CZQPr0qtxGVAmP3P1xN_Ihu_t9wzZ71ea6KNnbJcGt9sRp93T6o0P67zDiqQ7k57CrWE-GjxVHKuDVC6q0JtUeNnZb3nAQYV7Eiu5FS62rkQZ7KO03fRazIqUqzz3r1a3jg-eloZsLg12XRJt72d0uuQ8rjamtSOYi2MF8y_4j8102tuuhPfeKCm5uG8nb5x4p0CYLRc9iX_z996pAC2llnPAh61wgc_oSOLpnrR489F5nRudy-TQJFpEV6TUAWDyNtFWzO1lIQVYIMQRLYzrJB5vaWvphkGmVSXIU5ybKXMs0f2z4TCnaIMsWHdkS7f-Uj2VABaR2UN3aW-DX4A7az6Z8 \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..7e6e638 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.11-slim + +WORKDIR /app + +# System deps for psycopg2-binary are bundled, but keep image lean. +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/api/__init__.py b/backend/api/__init__.py new file mode 100644 index 0000000..a8cac65 --- /dev/null +++ b/backend/api/__init__.py @@ -0,0 +1 @@ +"""API package for the Migration Evaluation Platform (MEP).""" diff --git a/backend/api/routes/__init__.py b/backend/api/routes/__init__.py new file mode 100644 index 0000000..2795fe0 --- /dev/null +++ b/backend/api/routes/__init__.py @@ -0,0 +1 @@ +"""API route modules for the Migration Evaluation Platform (MEP).""" diff --git a/backend/api/routes/health.py b/backend/api/routes/health.py new file mode 100644 index 0000000..d2165ce --- /dev/null +++ b/backend/api/routes/health.py @@ -0,0 +1,26 @@ +"""Health-check route for the Migration Evaluation Platform (MEP).""" +from fastapi import APIRouter + +from config import settings +from database.connection import check_db_connection + +router = APIRouter(tags=["health"]) + +# Application version, kept in sync with the FastAPI app version. +VERSION = "0.1.0" + + +@router.get("/health") +def health() -> dict: + """Report service health. + + Always returns HTTP 200. If the database is unreachable, the response still + succeeds but reports ``"database": "disconnected"``. + """ + db_connected = check_db_connection() + return { + "status": "healthy", + "version": VERSION, + "environment": settings.APP_ENV, + "database": "connected" if db_connected else "disconnected", + } diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..799b2c3 --- /dev/null +++ b/backend/config.py @@ -0,0 +1,30 @@ +"""Application configuration for the Migration Evaluation Platform (MEP). + +Settings are loaded from environment variables and/or a local ``.env`` file +using pydantic-settings. +""" +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + """MEP backend settings.""" + + # Database connection string, e.g. + # postgresql://mep_user:mep_password@db:5432/mep_db + DATABASE_URL: str = "postgresql://mep_user:mep_password@db:5432/mep_db" + + # Runtime environment: development | staging | production + APP_ENV: str = "development" + + # Enable debug behaviour (verbose logging, etc.) + DEBUG: bool = True + + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + extra="ignore", + ) + + +# Single, importable settings instance used across the app. +settings = Settings() diff --git a/backend/database/__init__.py b/backend/database/__init__.py new file mode 100644 index 0000000..79fcb95 --- /dev/null +++ b/backend/database/__init__.py @@ -0,0 +1 @@ +"""Database package for the Migration Evaluation Platform (MEP).""" diff --git a/backend/database/connection.py b/backend/database/connection.py new file mode 100644 index 0000000..03ad1c3 --- /dev/null +++ b/backend/database/connection.py @@ -0,0 +1,58 @@ +"""Database engine, session factory, and connection helpers for MEP. + +Provides a synchronous SQLAlchemy engine built from ``settings.DATABASE_URL``, +a session factory, a ``get_db`` context manager / FastAPI dependency, and a +``check_db_connection`` helper used by the health endpoint. +""" +import logging +from contextlib import contextmanager +from typing import Generator + +from sqlalchemy import create_engine, text +from sqlalchemy.orm import Session, sessionmaker + +from config import settings + +logger = logging.getLogger(__name__) + +# Create the SQLAlchemy engine. ``pool_pre_ping`` transparently recycles stale +# connections, which is helpful when the database container restarts. +engine = create_engine( + settings.DATABASE_URL, + pool_pre_ping=True, + future=True, +) + +# Session factory bound to the engine. +SessionLocal = sessionmaker( + bind=engine, + autoflush=False, + autocommit=False, + expire_on_commit=False, + class_=Session, +) + + +@contextmanager +def get_db() -> Generator[Session, None, None]: + """Yield a database session, ensuring it is closed afterwards. + + Usable both as a context manager (``with get_db() as db:``) and as a + FastAPI dependency (``db: Session = Depends(get_db)``). + """ + db = SessionLocal() + try: + yield db + finally: + db.close() + + +def check_db_connection() -> bool: + """Return ``True`` if a simple ``SELECT 1`` succeeds, else ``False``.""" + try: + with engine.connect() as connection: + connection.execute(text("SELECT 1")) + return True + except Exception as exc: # noqa: BLE001 - health check must never raise + logger.warning("Database connection check failed: %s", exc) + return False diff --git a/backend/database/models.py b/backend/database/models.py new file mode 100644 index 0000000..51ff44d --- /dev/null +++ b/backend/database/models.py @@ -0,0 +1,12 @@ +"""SQLAlchemy declarative base and ORM models for MEP. + +This module currently only exposes the declarative ``Base``. MEP metadata +models (e.g. ``MigrationRun``, ``UploadedFile``) will be added in later steps. +""" +from sqlalchemy.orm import DeclarativeBase + + +class Base(DeclarativeBase): + """Base declarative class for all MEP ORM models.""" + + pass diff --git a/backend/evaluation/__init__.py b/backend/evaluation/__init__.py new file mode 100644 index 0000000..fc45eea --- /dev/null +++ b/backend/evaluation/__init__.py @@ -0,0 +1 @@ +"""Evaluation package (migration-quality evaluation) for MEP.""" diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..cb559a7 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,51 @@ +"""FastAPI application entrypoint for the Migration Evaluation Platform (MEP). + +This is the foundation-only skeleton: it wires up CORS, the health router, a +root route, and a startup hook that logs and tests the database connection. +Business logic (migration orchestration, evaluation, reporting) is added in +later steps. +""" +import logging + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from api.routes import health +from config import settings +from database.connection import check_db_connection + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("mep") + +VERSION = "0.1.0" + +app = FastAPI(title="Migration Evaluation Platform", version=VERSION) + +# CORS — allow all origins during development. +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Mount routers under the /api prefix. +app.include_router(health.router, prefix="/api") + + +@app.on_event("startup") +def on_startup() -> None: + """Log startup and test the database connection.""" + logger.info("MEP Backend starting...") + logger.info("Environment: %s | Debug: %s", settings.APP_ENV, settings.DEBUG) + if check_db_connection(): + logger.info("Database connection: OK") + else: + logger.warning("Database connection: UNAVAILABLE") + + +@app.get("/") +def root() -> dict: + """Root route confirming the API is running.""" + return {"message": "MEP API is running", "version": VERSION} diff --git a/backend/migration/README.md b/backend/migration/README.md new file mode 100644 index 0000000..dc87479 --- /dev/null +++ b/backend/migration/README.md @@ -0,0 +1,29 @@ +# Migration Engine + +Original migration engine — wrapped by FastAPI. + +This directory contains the original `PostgreDataMigrationApp` codebase, preserved +unchanged and relocated here as part of the Migration Evaluation Platform (MEP) v2 +restructure. Nothing here was rewritten — files were only moved. + +## Contents + +| Path | Description | +|------|-------------| +| `build/` | "Everything that ships" — the SQL-first, multi-engine database framework and the CSV → PostgreSQL loader/validator pipeline (`csv_loader.sh`, `csv/validator.py`, `csv/loader_postgresql.sh`). | +| `evals/` | Data-driven black-box scenarios (23 CSV edge-case scenarios across tiers P/I/S) plus the eval runner and VCRM gap report generator. | +| `infra/` | Containerisation (`Dockerfile`, `entrypoint.sh`) and Azure Terraform IaC. | +| `tests/` | Correctness coverage — 85 SQL assertions across 5 suites plus 9 Python test modules. | + +## Role within MEP + +The reusable "migration engine" is the `build/csv_loader.sh` + `build/csv/validator.py` ++ `build/csv/loader_postgresql.sh` pipeline (CSV → PostgreSQL via `COPY`). The MEP +FastAPI backend (`backend/api/`, `backend/services/`) wraps this engine rather than +reimplementing CSV → PostgreSQL loading. + +> **Note:** Internal path references inside `evals/runner.py` and `tests/*` were written +> relative to the original repository root (e.g. `Path(__file__).parents[1] / "build"`). +> Because `build/`, `evals/`, `infra/` and `tests/` were moved together into +> `backend/migration/`, their relative layout is preserved. Any absolute-from-root +> references should be re-verified when the FastAPI wrapper is wired up. diff --git a/build/adapters/adapter_influxdb.sh b/backend/migration/build/adapters/adapter_influxdb.sh similarity index 100% rename from build/adapters/adapter_influxdb.sh rename to backend/migration/build/adapters/adapter_influxdb.sh diff --git a/build/adapters/adapter_mariadb.sh b/backend/migration/build/adapters/adapter_mariadb.sh similarity index 100% rename from build/adapters/adapter_mariadb.sh rename to backend/migration/build/adapters/adapter_mariadb.sh diff --git a/build/adapters/adapter_postgresql.sh b/backend/migration/build/adapters/adapter_postgresql.sh similarity index 100% rename from build/adapters/adapter_postgresql.sh rename to backend/migration/build/adapters/adapter_postgresql.sh diff --git a/build/adapters/adapter_redis.sh b/backend/migration/build/adapters/adapter_redis.sh similarity index 100% rename from build/adapters/adapter_redis.sh rename to backend/migration/build/adapters/adapter_redis.sh diff --git a/build/adapters/adapter_sqlite.sh b/backend/migration/build/adapters/adapter_sqlite.sh similarity index 100% rename from build/adapters/adapter_sqlite.sh rename to backend/migration/build/adapters/adapter_sqlite.sh diff --git a/build/adapters/adapter_teradata.sh b/backend/migration/build/adapters/adapter_teradata.sh similarity index 100% rename from build/adapters/adapter_teradata.sh rename to backend/migration/build/adapters/adapter_teradata.sh diff --git a/build/config.env.example b/backend/migration/build/config.env.example similarity index 100% rename from build/config.env.example rename to backend/migration/build/config.env.example diff --git a/build/csv/loader_influxdb.sh b/backend/migration/build/csv/loader_influxdb.sh similarity index 100% rename from build/csv/loader_influxdb.sh rename to backend/migration/build/csv/loader_influxdb.sh diff --git a/build/csv/loader_mariadb.sh b/backend/migration/build/csv/loader_mariadb.sh similarity index 100% rename from build/csv/loader_mariadb.sh rename to backend/migration/build/csv/loader_mariadb.sh diff --git a/build/csv/loader_postgresql.sh b/backend/migration/build/csv/loader_postgresql.sh similarity index 100% rename from build/csv/loader_postgresql.sh rename to backend/migration/build/csv/loader_postgresql.sh diff --git a/build/csv/loader_redis.sh b/backend/migration/build/csv/loader_redis.sh similarity index 100% rename from build/csv/loader_redis.sh rename to backend/migration/build/csv/loader_redis.sh diff --git a/build/csv/loader_sqlite.sh b/backend/migration/build/csv/loader_sqlite.sh similarity index 100% rename from build/csv/loader_sqlite.sh rename to backend/migration/build/csv/loader_sqlite.sh diff --git a/build/csv/loader_teradata.sh b/backend/migration/build/csv/loader_teradata.sh similarity index 100% rename from build/csv/loader_teradata.sh rename to backend/migration/build/csv/loader_teradata.sh diff --git a/build/csv/samples/customers.csv b/backend/migration/build/csv/samples/customers.csv similarity index 100% rename from build/csv/samples/customers.csv rename to backend/migration/build/csv/samples/customers.csv diff --git a/build/csv/samples/inventory.csv b/backend/migration/build/csv/samples/inventory.csv similarity index 100% rename from build/csv/samples/inventory.csv rename to backend/migration/build/csv/samples/inventory.csv diff --git a/build/csv/samples/orders.csv b/backend/migration/build/csv/samples/orders.csv similarity index 100% rename from build/csv/samples/orders.csv rename to backend/migration/build/csv/samples/orders.csv diff --git a/build/csv/validator.py b/backend/migration/build/csv/validator.py similarity index 100% rename from build/csv/validator.py rename to backend/migration/build/csv/validator.py diff --git a/build/csv/validator.sh b/backend/migration/build/csv/validator.sh similarity index 100% rename from build/csv/validator.sh rename to backend/migration/build/csv/validator.sh diff --git a/build/csv_loader.sh b/backend/migration/build/csv_loader.sh similarity index 100% rename from build/csv_loader.sh rename to backend/migration/build/csv_loader.sh diff --git a/build/csv_utilise.sh b/backend/migration/build/csv_utilise.sh similarity index 100% rename from build/csv_utilise.sh rename to backend/migration/build/csv_utilise.sh diff --git a/build/deploy_all.sh b/backend/migration/build/deploy_all.sh similarity index 100% rename from build/deploy_all.sh rename to backend/migration/build/deploy_all.sh diff --git a/build/environments/env_dev.sql b/backend/migration/build/environments/env_dev.sql similarity index 100% rename from build/environments/env_dev.sql rename to backend/migration/build/environments/env_dev.sql diff --git a/build/environments/env_prod.sql b/backend/migration/build/environments/env_prod.sql similarity index 100% rename from build/environments/env_prod.sql rename to backend/migration/build/environments/env_prod.sql diff --git a/build/environments/env_staging.sql b/backend/migration/build/environments/env_staging.sql similarity index 100% rename from build/environments/env_staging.sql rename to backend/migration/build/environments/env_staging.sql diff --git a/build/environments/env_test.sql b/backend/migration/build/environments/env_test.sql similarity index 100% rename from build/environments/env_test.sql rename to backend/migration/build/environments/env_test.sql diff --git a/build/schema/influxdb/te_seed_data.lp b/backend/migration/build/schema/influxdb/te_seed_data.lp similarity index 100% rename from build/schema/influxdb/te_seed_data.lp rename to backend/migration/build/schema/influxdb/te_seed_data.lp diff --git a/build/schema/mariadb/te_core_schema.sql b/backend/migration/build/schema/mariadb/te_core_schema.sql similarity index 100% rename from build/schema/mariadb/te_core_schema.sql rename to backend/migration/build/schema/mariadb/te_core_schema.sql diff --git a/build/schema/mariadb/te_seed_data.sql b/backend/migration/build/schema/mariadb/te_seed_data.sql similarity index 100% rename from build/schema/mariadb/te_seed_data.sql rename to backend/migration/build/schema/mariadb/te_seed_data.sql diff --git a/build/schema/postgresql/te_core_schema.sql b/backend/migration/build/schema/postgresql/te_core_schema.sql similarity index 100% rename from build/schema/postgresql/te_core_schema.sql rename to backend/migration/build/schema/postgresql/te_core_schema.sql diff --git a/build/schema/postgresql/te_seed_data.sql b/backend/migration/build/schema/postgresql/te_seed_data.sql similarity index 100% rename from build/schema/postgresql/te_seed_data.sql rename to backend/migration/build/schema/postgresql/te_seed_data.sql diff --git a/build/schema/redis/te_seed_data.sh b/backend/migration/build/schema/redis/te_seed_data.sh similarity index 100% rename from build/schema/redis/te_seed_data.sh rename to backend/migration/build/schema/redis/te_seed_data.sh diff --git a/build/schema/sqlite/te_core_schema.sql b/backend/migration/build/schema/sqlite/te_core_schema.sql similarity index 100% rename from build/schema/sqlite/te_core_schema.sql rename to backend/migration/build/schema/sqlite/te_core_schema.sql diff --git a/build/schema/sqlite/te_seed_data.sql b/backend/migration/build/schema/sqlite/te_seed_data.sql similarity index 100% rename from build/schema/sqlite/te_seed_data.sql rename to backend/migration/build/schema/sqlite/te_seed_data.sql diff --git a/build/schema/teradata/te_core_schema.sql b/backend/migration/build/schema/teradata/te_core_schema.sql similarity index 100% rename from build/schema/teradata/te_core_schema.sql rename to backend/migration/build/schema/teradata/te_core_schema.sql diff --git a/build/schema/teradata/te_seed_data.sql b/backend/migration/build/schema/teradata/te_seed_data.sql similarity index 100% rename from build/schema/teradata/te_seed_data.sql rename to backend/migration/build/schema/teradata/te_seed_data.sql diff --git a/build/setup.sh b/backend/migration/build/setup.sh similarity index 100% rename from build/setup.sh rename to backend/migration/build/setup.sh diff --git a/build/te_core_schema.sql b/backend/migration/build/te_core_schema.sql similarity index 100% rename from build/te_core_schema.sql rename to backend/migration/build/te_core_schema.sql diff --git a/build/te_seed_data.sql b/backend/migration/build/te_seed_data.sql similarity index 100% rename from build/te_seed_data.sql rename to backend/migration/build/te_seed_data.sql diff --git a/build/terraform-github-repos/.gitignore b/backend/migration/build/terraform-github-repos/.gitignore similarity index 100% rename from build/terraform-github-repos/.gitignore rename to backend/migration/build/terraform-github-repos/.gitignore diff --git a/build/terraform-github-repos/main.tf b/backend/migration/build/terraform-github-repos/main.tf similarity index 100% rename from build/terraform-github-repos/main.tf rename to backend/migration/build/terraform-github-repos/main.tf diff --git a/build/terraform-github-repos/outputs.tf b/backend/migration/build/terraform-github-repos/outputs.tf similarity index 100% rename from build/terraform-github-repos/outputs.tf rename to backend/migration/build/terraform-github-repos/outputs.tf diff --git a/build/terraform-github-repos/terraform.tfvars.example b/backend/migration/build/terraform-github-repos/terraform.tfvars.example similarity index 100% rename from build/terraform-github-repos/terraform.tfvars.example rename to backend/migration/build/terraform-github-repos/terraform.tfvars.example diff --git a/build/terraform-github-repos/variables.tf b/backend/migration/build/terraform-github-repos/variables.tf similarity index 100% rename from build/terraform-github-repos/variables.tf rename to backend/migration/build/terraform-github-repos/variables.tf diff --git a/evals/FAILURE_MODES.md b/backend/migration/evals/FAILURE_MODES.md similarity index 100% rename from evals/FAILURE_MODES.md rename to backend/migration/evals/FAILURE_MODES.md diff --git a/evals/HANDOFF.md b/backend/migration/evals/HANDOFF.md similarity index 100% rename from evals/HANDOFF.md rename to backend/migration/evals/HANDOFF.md diff --git a/evals/PLAN.md b/backend/migration/evals/PLAN.md similarity index 100% rename from evals/PLAN.md rename to backend/migration/evals/PLAN.md diff --git a/evals/USAGE.md b/backend/migration/evals/USAGE.md similarity index 100% rename from evals/USAGE.md rename to backend/migration/evals/USAGE.md diff --git a/evals/datasets/tier_i/01_deploy_dev_twice/NOTES.txt b/backend/migration/evals/datasets/tier_i/01_deploy_dev_twice/NOTES.txt similarity index 100% rename from evals/datasets/tier_i/01_deploy_dev_twice/NOTES.txt rename to backend/migration/evals/datasets/tier_i/01_deploy_dev_twice/NOTES.txt diff --git a/evals/datasets/tier_p/01_happy_path/input.csv b/backend/migration/evals/datasets/tier_p/01_happy_path/input.csv similarity index 100% rename from evals/datasets/tier_p/01_happy_path/input.csv rename to backend/migration/evals/datasets/tier_p/01_happy_path/input.csv diff --git a/backend/migration/evals/datasets/tier_p/02_empty_file/input.csv b/backend/migration/evals/datasets/tier_p/02_empty_file/input.csv new file mode 100644 index 0000000..e69de29 diff --git a/evals/datasets/tier_p/03_empty_header_only_newline/input.csv b/backend/migration/evals/datasets/tier_p/03_empty_header_only_newline/input.csv similarity index 100% rename from evals/datasets/tier_p/03_empty_header_only_newline/input.csv rename to backend/migration/evals/datasets/tier_p/03_empty_header_only_newline/input.csv diff --git a/evals/datasets/tier_p/04_no_valid_rows/input.csv b/backend/migration/evals/datasets/tier_p/04_no_valid_rows/input.csv similarity index 100% rename from evals/datasets/tier_p/04_no_valid_rows/input.csv rename to backend/migration/evals/datasets/tier_p/04_no_valid_rows/input.csv diff --git a/evals/datasets/tier_p/05_mixed_valid_skipped/input.csv b/backend/migration/evals/datasets/tier_p/05_mixed_valid_skipped/input.csv similarity index 100% rename from evals/datasets/tier_p/05_mixed_valid_skipped/input.csv rename to backend/migration/evals/datasets/tier_p/05_mixed_valid_skipped/input.csv diff --git a/evals/datasets/tier_p/06_duplicate_headers/input.csv b/backend/migration/evals/datasets/tier_p/06_duplicate_headers/input.csv similarity index 100% rename from evals/datasets/tier_p/06_duplicate_headers/input.csv rename to backend/migration/evals/datasets/tier_p/06_duplicate_headers/input.csv diff --git a/evals/datasets/tier_p/07_column_mismatch_short/input.csv b/backend/migration/evals/datasets/tier_p/07_column_mismatch_short/input.csv similarity index 100% rename from evals/datasets/tier_p/07_column_mismatch_short/input.csv rename to backend/migration/evals/datasets/tier_p/07_column_mismatch_short/input.csv diff --git a/evals/datasets/tier_p/08_column_mismatch_long/input.csv b/backend/migration/evals/datasets/tier_p/08_column_mismatch_long/input.csv similarity index 100% rename from evals/datasets/tier_p/08_column_mismatch_long/input.csv rename to backend/migration/evals/datasets/tier_p/08_column_mismatch_long/input.csv diff --git a/evals/datasets/tier_p/09_empty_row/input.csv b/backend/migration/evals/datasets/tier_p/09_empty_row/input.csv similarity index 100% rename from evals/datasets/tier_p/09_empty_row/input.csv rename to backend/migration/evals/datasets/tier_p/09_empty_row/input.csv diff --git a/evals/datasets/tier_p/10_utf8_bom/input.csv b/backend/migration/evals/datasets/tier_p/10_utf8_bom/input.csv similarity index 100% rename from evals/datasets/tier_p/10_utf8_bom/input.csv rename to backend/migration/evals/datasets/tier_p/10_utf8_bom/input.csv diff --git a/evals/datasets/tier_p/11_utf8_emoji/input.csv b/backend/migration/evals/datasets/tier_p/11_utf8_emoji/input.csv similarity index 100% rename from evals/datasets/tier_p/11_utf8_emoji/input.csv rename to backend/migration/evals/datasets/tier_p/11_utf8_emoji/input.csv diff --git a/evals/datasets/tier_p/12_crlf_line_endings/input.csv b/backend/migration/evals/datasets/tier_p/12_crlf_line_endings/input.csv similarity index 100% rename from evals/datasets/tier_p/12_crlf_line_endings/input.csv rename to backend/migration/evals/datasets/tier_p/12_crlf_line_endings/input.csv diff --git a/evals/datasets/tier_p/13_quoted_comma/input.csv b/backend/migration/evals/datasets/tier_p/13_quoted_comma/input.csv similarity index 100% rename from evals/datasets/tier_p/13_quoted_comma/input.csv rename to backend/migration/evals/datasets/tier_p/13_quoted_comma/input.csv diff --git a/evals/datasets/tier_p/14_quoted_newline/input.csv b/backend/migration/evals/datasets/tier_p/14_quoted_newline/input.csv similarity index 100% rename from evals/datasets/tier_p/14_quoted_newline/input.csv rename to backend/migration/evals/datasets/tier_p/14_quoted_newline/input.csv diff --git a/evals/datasets/tier_p/15_quoted_quote/input.csv b/backend/migration/evals/datasets/tier_p/15_quoted_quote/input.csv similarity index 100% rename from evals/datasets/tier_p/15_quoted_quote/input.csv rename to backend/migration/evals/datasets/tier_p/15_quoted_quote/input.csv diff --git a/evals/datasets/tier_p/16_whitespace_only_row/input.csv b/backend/migration/evals/datasets/tier_p/16_whitespace_only_row/input.csv similarity index 100% rename from evals/datasets/tier_p/16_whitespace_only_row/input.csv rename to backend/migration/evals/datasets/tier_p/16_whitespace_only_row/input.csv diff --git a/evals/datasets/tier_p/17_header_whitespace/input.csv b/backend/migration/evals/datasets/tier_p/17_header_whitespace/input.csv similarity index 100% rename from evals/datasets/tier_p/17_header_whitespace/input.csv rename to backend/migration/evals/datasets/tier_p/17_header_whitespace/input.csv diff --git a/evals/datasets/tier_p/18_utf8_cjk/input.csv b/backend/migration/evals/datasets/tier_p/18_utf8_cjk/input.csv similarity index 100% rename from evals/datasets/tier_p/18_utf8_cjk/input.csv rename to backend/migration/evals/datasets/tier_p/18_utf8_cjk/input.csv diff --git a/evals/datasets/tier_p/19_missing_env_vars/README.txt b/backend/migration/evals/datasets/tier_p/19_missing_env_vars/README.txt similarity index 100% rename from evals/datasets/tier_p/19_missing_env_vars/README.txt rename to backend/migration/evals/datasets/tier_p/19_missing_env_vars/README.txt diff --git a/evals/datasets/tier_p/20_missing_csv_file/README.txt b/backend/migration/evals/datasets/tier_p/20_missing_csv_file/README.txt similarity index 100% rename from evals/datasets/tier_p/20_missing_csv_file/README.txt rename to backend/migration/evals/datasets/tier_p/20_missing_csv_file/README.txt diff --git a/evals/datasets/tier_p/21_utf8_arabic/input.csv b/backend/migration/evals/datasets/tier_p/21_utf8_arabic/input.csv similarity index 100% rename from evals/datasets/tier_p/21_utf8_arabic/input.csv rename to backend/migration/evals/datasets/tier_p/21_utf8_arabic/input.csv diff --git a/evals/datasets/tier_p/22_very_long_field/README.txt b/backend/migration/evals/datasets/tier_p/22_very_long_field/README.txt similarity index 100% rename from evals/datasets/tier_p/22_very_long_field/README.txt rename to backend/migration/evals/datasets/tier_p/22_very_long_field/README.txt diff --git a/evals/datasets/tier_p/23_invalid_utf8_bytes/README.txt b/backend/migration/evals/datasets/tier_p/23_invalid_utf8_bytes/README.txt similarity index 100% rename from evals/datasets/tier_p/23_invalid_utf8_bytes/README.txt rename to backend/migration/evals/datasets/tier_p/23_invalid_utf8_bytes/README.txt diff --git a/evals/datasets/tier_s/01_fresh_deploy_then_all_tests_pass/NOTES.txt b/backend/migration/evals/datasets/tier_s/01_fresh_deploy_then_all_tests_pass/NOTES.txt similarity index 100% rename from evals/datasets/tier_s/01_fresh_deploy_then_all_tests_pass/NOTES.txt rename to backend/migration/evals/datasets/tier_s/01_fresh_deploy_then_all_tests_pass/NOTES.txt diff --git a/evals/expected/tier_i/01_deploy_dev_twice.json b/backend/migration/evals/expected/tier_i/01_deploy_dev_twice.json similarity index 100% rename from evals/expected/tier_i/01_deploy_dev_twice.json rename to backend/migration/evals/expected/tier_i/01_deploy_dev_twice.json diff --git a/evals/expected/tier_p/01_happy_path.json b/backend/migration/evals/expected/tier_p/01_happy_path.json similarity index 100% rename from evals/expected/tier_p/01_happy_path.json rename to backend/migration/evals/expected/tier_p/01_happy_path.json diff --git a/evals/expected/tier_p/02_empty_file.json b/backend/migration/evals/expected/tier_p/02_empty_file.json similarity index 100% rename from evals/expected/tier_p/02_empty_file.json rename to backend/migration/evals/expected/tier_p/02_empty_file.json diff --git a/evals/expected/tier_p/03_empty_header_only_newline.json b/backend/migration/evals/expected/tier_p/03_empty_header_only_newline.json similarity index 100% rename from evals/expected/tier_p/03_empty_header_only_newline.json rename to backend/migration/evals/expected/tier_p/03_empty_header_only_newline.json diff --git a/evals/expected/tier_p/04_no_valid_rows.json b/backend/migration/evals/expected/tier_p/04_no_valid_rows.json similarity index 100% rename from evals/expected/tier_p/04_no_valid_rows.json rename to backend/migration/evals/expected/tier_p/04_no_valid_rows.json diff --git a/evals/expected/tier_p/05_mixed_valid_skipped.json b/backend/migration/evals/expected/tier_p/05_mixed_valid_skipped.json similarity index 100% rename from evals/expected/tier_p/05_mixed_valid_skipped.json rename to backend/migration/evals/expected/tier_p/05_mixed_valid_skipped.json diff --git a/evals/expected/tier_p/06_duplicate_headers.json b/backend/migration/evals/expected/tier_p/06_duplicate_headers.json similarity index 100% rename from evals/expected/tier_p/06_duplicate_headers.json rename to backend/migration/evals/expected/tier_p/06_duplicate_headers.json diff --git a/evals/expected/tier_p/07_column_mismatch_short.json b/backend/migration/evals/expected/tier_p/07_column_mismatch_short.json similarity index 100% rename from evals/expected/tier_p/07_column_mismatch_short.json rename to backend/migration/evals/expected/tier_p/07_column_mismatch_short.json diff --git a/evals/expected/tier_p/08_column_mismatch_long.json b/backend/migration/evals/expected/tier_p/08_column_mismatch_long.json similarity index 100% rename from evals/expected/tier_p/08_column_mismatch_long.json rename to backend/migration/evals/expected/tier_p/08_column_mismatch_long.json diff --git a/evals/expected/tier_p/09_empty_row.json b/backend/migration/evals/expected/tier_p/09_empty_row.json similarity index 100% rename from evals/expected/tier_p/09_empty_row.json rename to backend/migration/evals/expected/tier_p/09_empty_row.json diff --git a/evals/expected/tier_p/10_utf8_bom.json b/backend/migration/evals/expected/tier_p/10_utf8_bom.json similarity index 100% rename from evals/expected/tier_p/10_utf8_bom.json rename to backend/migration/evals/expected/tier_p/10_utf8_bom.json diff --git a/evals/expected/tier_p/11_utf8_emoji.json b/backend/migration/evals/expected/tier_p/11_utf8_emoji.json similarity index 100% rename from evals/expected/tier_p/11_utf8_emoji.json rename to backend/migration/evals/expected/tier_p/11_utf8_emoji.json diff --git a/evals/expected/tier_p/12_crlf_line_endings.json b/backend/migration/evals/expected/tier_p/12_crlf_line_endings.json similarity index 100% rename from evals/expected/tier_p/12_crlf_line_endings.json rename to backend/migration/evals/expected/tier_p/12_crlf_line_endings.json diff --git a/evals/expected/tier_p/13_quoted_comma.json b/backend/migration/evals/expected/tier_p/13_quoted_comma.json similarity index 100% rename from evals/expected/tier_p/13_quoted_comma.json rename to backend/migration/evals/expected/tier_p/13_quoted_comma.json diff --git a/evals/expected/tier_p/14_quoted_newline.json b/backend/migration/evals/expected/tier_p/14_quoted_newline.json similarity index 100% rename from evals/expected/tier_p/14_quoted_newline.json rename to backend/migration/evals/expected/tier_p/14_quoted_newline.json diff --git a/evals/expected/tier_p/15_quoted_quote.json b/backend/migration/evals/expected/tier_p/15_quoted_quote.json similarity index 100% rename from evals/expected/tier_p/15_quoted_quote.json rename to backend/migration/evals/expected/tier_p/15_quoted_quote.json diff --git a/evals/expected/tier_p/16_whitespace_only_row.json b/backend/migration/evals/expected/tier_p/16_whitespace_only_row.json similarity index 100% rename from evals/expected/tier_p/16_whitespace_only_row.json rename to backend/migration/evals/expected/tier_p/16_whitespace_only_row.json diff --git a/evals/expected/tier_p/17_header_whitespace.json b/backend/migration/evals/expected/tier_p/17_header_whitespace.json similarity index 100% rename from evals/expected/tier_p/17_header_whitespace.json rename to backend/migration/evals/expected/tier_p/17_header_whitespace.json diff --git a/evals/expected/tier_p/18_utf8_cjk.json b/backend/migration/evals/expected/tier_p/18_utf8_cjk.json similarity index 100% rename from evals/expected/tier_p/18_utf8_cjk.json rename to backend/migration/evals/expected/tier_p/18_utf8_cjk.json diff --git a/evals/expected/tier_p/19_missing_env_vars.json b/backend/migration/evals/expected/tier_p/19_missing_env_vars.json similarity index 100% rename from evals/expected/tier_p/19_missing_env_vars.json rename to backend/migration/evals/expected/tier_p/19_missing_env_vars.json diff --git a/evals/expected/tier_p/20_missing_csv_file.json b/backend/migration/evals/expected/tier_p/20_missing_csv_file.json similarity index 100% rename from evals/expected/tier_p/20_missing_csv_file.json rename to backend/migration/evals/expected/tier_p/20_missing_csv_file.json diff --git a/evals/expected/tier_p/21_utf8_arabic.json b/backend/migration/evals/expected/tier_p/21_utf8_arabic.json similarity index 100% rename from evals/expected/tier_p/21_utf8_arabic.json rename to backend/migration/evals/expected/tier_p/21_utf8_arabic.json diff --git a/evals/expected/tier_p/22_very_long_field.json b/backend/migration/evals/expected/tier_p/22_very_long_field.json similarity index 100% rename from evals/expected/tier_p/22_very_long_field.json rename to backend/migration/evals/expected/tier_p/22_very_long_field.json diff --git a/evals/expected/tier_p/23_invalid_utf8_bytes.json b/backend/migration/evals/expected/tier_p/23_invalid_utf8_bytes.json similarity index 100% rename from evals/expected/tier_p/23_invalid_utf8_bytes.json rename to backend/migration/evals/expected/tier_p/23_invalid_utf8_bytes.json diff --git a/evals/expected/tier_s/01_fresh_deploy_then_all_tests_pass.json b/backend/migration/evals/expected/tier_s/01_fresh_deploy_then_all_tests_pass.json similarity index 100% rename from evals/expected/tier_s/01_fresh_deploy_then_all_tests_pass.json rename to backend/migration/evals/expected/tier_s/01_fresh_deploy_then_all_tests_pass.json diff --git a/evals/gap_report.py b/backend/migration/evals/gap_report.py similarity index 100% rename from evals/gap_report.py rename to backend/migration/evals/gap_report.py diff --git a/evals/runner.py b/backend/migration/evals/runner.py similarity index 100% rename from evals/runner.py rename to backend/migration/evals/runner.py diff --git a/infra/.dockerignore b/backend/migration/infra/.dockerignore similarity index 100% rename from infra/.dockerignore rename to backend/migration/infra/.dockerignore diff --git a/infra/Dockerfile b/backend/migration/infra/Dockerfile similarity index 100% rename from infra/Dockerfile rename to backend/migration/infra/Dockerfile diff --git a/infra/azure-portal-walkthrough.md b/backend/migration/infra/azure-portal-walkthrough.md similarity index 100% rename from infra/azure-portal-walkthrough.md rename to backend/migration/infra/azure-portal-walkthrough.md diff --git a/infra/entrypoint.sh b/backend/migration/infra/entrypoint.sh similarity index 100% rename from infra/entrypoint.sh rename to backend/migration/infra/entrypoint.sh diff --git a/infra/terraform-prod/.gitignore b/backend/migration/infra/terraform-prod/.gitignore similarity index 100% rename from infra/terraform-prod/.gitignore rename to backend/migration/infra/terraform-prod/.gitignore diff --git a/infra/terraform-prod/main.tf b/backend/migration/infra/terraform-prod/main.tf similarity index 100% rename from infra/terraform-prod/main.tf rename to backend/migration/infra/terraform-prod/main.tf diff --git a/infra/terraform-prod/outputs.tf b/backend/migration/infra/terraform-prod/outputs.tf similarity index 100% rename from infra/terraform-prod/outputs.tf rename to backend/migration/infra/terraform-prod/outputs.tf diff --git a/infra/terraform-prod/terraform.tfvars.example b/backend/migration/infra/terraform-prod/terraform.tfvars.example similarity index 100% rename from infra/terraform-prod/terraform.tfvars.example rename to backend/migration/infra/terraform-prod/terraform.tfvars.example diff --git a/infra/terraform-prod/variables.tf b/backend/migration/infra/terraform-prod/variables.tf similarity index 100% rename from infra/terraform-prod/variables.tf rename to backend/migration/infra/terraform-prod/variables.tf diff --git a/infra/terraform/.gitignore b/backend/migration/infra/terraform/.gitignore similarity index 100% rename from infra/terraform/.gitignore rename to backend/migration/infra/terraform/.gitignore diff --git a/infra/terraform/main.tf b/backend/migration/infra/terraform/main.tf similarity index 100% rename from infra/terraform/main.tf rename to backend/migration/infra/terraform/main.tf diff --git a/infra/terraform/outputs.tf b/backend/migration/infra/terraform/outputs.tf similarity index 100% rename from infra/terraform/outputs.tf rename to backend/migration/infra/terraform/outputs.tf diff --git a/infra/terraform/terraform.tfvars.example b/backend/migration/infra/terraform/terraform.tfvars.example similarity index 100% rename from infra/terraform/terraform.tfvars.example rename to backend/migration/infra/terraform/terraform.tfvars.example diff --git a/infra/terraform/variables.tf b/backend/migration/infra/terraform/variables.tf similarity index 100% rename from infra/terraform/variables.tf rename to backend/migration/infra/terraform/variables.tf diff --git a/tests/conftest.py b/backend/migration/tests/conftest.py similarity index 100% rename from tests/conftest.py rename to backend/migration/tests/conftest.py diff --git a/tests/framework/test_framework.sql b/backend/migration/tests/framework/test_framework.sql similarity index 100% rename from tests/framework/test_framework.sql rename to backend/migration/tests/framework/test_framework.sql diff --git a/tests/run_all_tests.sql b/backend/migration/tests/run_all_tests.sql similarity index 100% rename from tests/run_all_tests.sql rename to backend/migration/tests/run_all_tests.sql diff --git a/tests/run_python_tests.ps1 b/backend/migration/tests/run_python_tests.ps1 similarity index 100% rename from tests/run_python_tests.ps1 rename to backend/migration/tests/run_python_tests.ps1 diff --git a/tests/run_tests.sh b/backend/migration/tests/run_tests.sh similarity index 100% rename from tests/run_tests.sh rename to backend/migration/tests/run_tests.sh diff --git a/tests/snapshots/basic_expected_skip.csv b/backend/migration/tests/snapshots/basic_expected_skip.csv similarity index 100% rename from tests/snapshots/basic_expected_skip.csv rename to backend/migration/tests/snapshots/basic_expected_skip.csv diff --git a/tests/snapshots/basic_expected_valid.csv b/backend/migration/tests/snapshots/basic_expected_valid.csv similarity index 100% rename from tests/snapshots/basic_expected_valid.csv rename to backend/migration/tests/snapshots/basic_expected_valid.csv diff --git a/tests/snapshots/basic_input.csv b/backend/migration/tests/snapshots/basic_input.csv similarity index 100% rename from tests/snapshots/basic_input.csv rename to backend/migration/tests/snapshots/basic_input.csv diff --git a/tests/suites/test_01_organisations_personnel.sql b/backend/migration/tests/suites/test_01_organisations_personnel.sql similarity index 100% rename from tests/suites/test_01_organisations_personnel.sql rename to backend/migration/tests/suites/test_01_organisations_personnel.sql diff --git a/tests/suites/test_02_programs_phases.sql b/backend/migration/tests/suites/test_02_programs_phases.sql similarity index 100% rename from tests/suites/test_02_programs_phases.sql rename to backend/migration/tests/suites/test_02_programs_phases.sql diff --git a/tests/suites/test_03_requirements_vcrm.sql b/backend/migration/tests/suites/test_03_requirements_vcrm.sql similarity index 100% rename from tests/suites/test_03_requirements_vcrm.sql rename to backend/migration/tests/suites/test_03_requirements_vcrm.sql diff --git a/tests/suites/test_04_execution_defects.sql b/backend/migration/tests/suites/test_04_execution_defects.sql similarity index 100% rename from tests/suites/test_04_execution_defects.sql rename to backend/migration/tests/suites/test_04_execution_defects.sql diff --git a/tests/suites/test_05_schema_and_business_rules.sql b/backend/migration/tests/suites/test_05_schema_and_business_rules.sql similarity index 100% rename from tests/suites/test_05_schema_and_business_rules.sql rename to backend/migration/tests/suites/test_05_schema_and_business_rules.sql diff --git a/tests/test_csv_loader_arbitrary_shapes.py b/backend/migration/tests/test_csv_loader_arbitrary_shapes.py similarity index 100% rename from tests/test_csv_loader_arbitrary_shapes.py rename to backend/migration/tests/test_csv_loader_arbitrary_shapes.py diff --git a/tests/test_csv_utilise.py b/backend/migration/tests/test_csv_utilise.py similarity index 100% rename from tests/test_csv_utilise.py rename to backend/migration/tests/test_csv_utilise.py diff --git a/tests/test_csv_validator.py b/backend/migration/tests/test_csv_validator.py similarity index 100% rename from tests/test_csv_validator.py rename to backend/migration/tests/test_csv_validator.py diff --git a/tests/test_e2e_pipeline.py b/backend/migration/tests/test_e2e_pipeline.py similarity index 100% rename from tests/test_e2e_pipeline.py rename to backend/migration/tests/test_e2e_pipeline.py diff --git a/tests/test_evals_runner.py b/backend/migration/tests/test_evals_runner.py similarity index 100% rename from tests/test_evals_runner.py rename to backend/migration/tests/test_evals_runner.py diff --git a/tests/test_parity.py b/backend/migration/tests/test_parity.py similarity index 100% rename from tests/test_parity.py rename to backend/migration/tests/test_parity.py diff --git a/tests/test_regression.py b/backend/migration/tests/test_regression.py similarity index 100% rename from tests/test_regression.py rename to backend/migration/tests/test_regression.py diff --git a/tests/test_security.py b/backend/migration/tests/test_security.py similarity index 100% rename from tests/test_security.py rename to backend/migration/tests/test_security.py diff --git a/tests/test_snapshot.py b/backend/migration/tests/test_snapshot.py similarity index 100% rename from tests/test_snapshot.py rename to backend/migration/tests/test_snapshot.py diff --git a/backend/reports/__init__.py b/backend/reports/__init__.py new file mode 100644 index 0000000..98714ac --- /dev/null +++ b/backend/reports/__init__.py @@ -0,0 +1 @@ +"""Reports package (reconciliation & evaluation reports) for MEP.""" diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..a6e19f6 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,11 @@ +fastapi>=0.110.0 +uvicorn[standard]>=0.29.0 +sqlalchemy>=2.0.0 +alembic>=1.13.0 +pydantic-settings>=2.0.0 +python-multipart +python-dotenv +psycopg2-binary +pytest +httpx +pytest-asyncio diff --git a/backend/services/__init__.py b/backend/services/__init__.py new file mode 100644 index 0000000..ce0902b --- /dev/null +++ b/backend/services/__init__.py @@ -0,0 +1 @@ +"""Services package (business logic) for MEP.""" diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..e1e29a3 --- /dev/null +++ b/backend/tests/__init__.py @@ -0,0 +1 @@ +"""Test package for the MEP backend.""" diff --git a/backend/tests/test_health.py b/backend/tests/test_health.py new file mode 100644 index 0000000..240da57 --- /dev/null +++ b/backend/tests/test_health.py @@ -0,0 +1,30 @@ +"""Smoke tests for the MEP backend skeleton.""" +from fastapi.testclient import TestClient + +from main import app + +client = TestClient(app) + + +def test_root_returns_200(): + """Root route responds with 200 and the running message.""" + response = client.get("/") + assert response.status_code == 200 + body = response.json() + assert body["message"] == "MEP API is running" + assert body["version"] == "0.1.0" + + +def test_health_returns_200_with_status(): + """Health route responds with 200 and a status field. + + The database may be unavailable in CI; the endpoint must still return 200 + and report the database as either connected or disconnected. + """ + response = client.get("/api/health") + assert response.status_code == 200 + body = response.json() + assert body["status"] == "healthy" + assert body["version"] == "0.1.0" + assert "environment" in body + assert body["database"] in ("connected", "disconnected") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5007b06 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,46 @@ +services: + db: + image: postgres:15-alpine + container_name: mep_db + environment: + POSTGRES_DB: mep_db + POSTGRES_USER: mep_user + POSTGRES_PASSWORD: mep_password + ports: + - "5432:5432" + volumes: + - mep_pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U mep_user -d mep_db"] + interval: 10s + timeout: 5s + retries: 5 + + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: mep_backend + ports: + - "8000:8000" + environment: + DATABASE_URL: postgresql://mep_user:mep_password@db:5432/mep_db + APP_ENV: development + depends_on: + db: + condition: service_healthy + volumes: + - ./uploads:/app/uploads + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: mep_frontend + ports: + - "3000:80" + depends_on: + - backend + +volumes: + mep_pgdata: diff --git a/docker/.gitkeep b/docker/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/ADR/ADR-001-Technology-Choices.md b/docs/ADR/ADR-001-Technology-Choices.md new file mode 100644 index 0000000..8d2f8b7 --- /dev/null +++ b/docs/ADR/ADR-001-Technology-Choices.md @@ -0,0 +1,31 @@ +# ADR-001: Technology Choices + +- **Status:** Accepted +- **Date:** 2026-07-14 + +## Context + +MEP needs a web frontend, an API layer, and a relational datastore to wrap the +existing CSV → PostgreSQL migration engine. We need a stack that is productive, +widely supported, and a good fit for the engine's existing PostgreSQL focus. + +## Decision + +- **Frontend: React + TypeScript + Material UI (MUI).** + React is the dominant SPA framework with a deep ecosystem and hiring pool. + TypeScript adds type safety. MUI provides a mature component library that + lets us mirror the Azure Portal look-and-feel quickly. +- **Backend: FastAPI (Python 3.11).** + The migration engine is already Python/Bash, so a Python API keeps one + language for backend logic and enables direct reuse of engine code. FastAPI + gives async performance, Pydantic validation, and automatic OpenAPI docs. +- **Database: PostgreSQL 15.** + The engine's primary, best-tested target is PostgreSQL, so using it for + application data keeps operational tooling and expertise consistent. + +## Consequences + +- One backend language (Python) across API and engine reduces context switching. +- Automatic OpenAPI docs and typed frontend contracts speed up development. +- MUI accelerates a polished, Azure-style UI at the cost of a heavier bundle. +- Committing to PostgreSQL is low-risk given the engine's existing reliance on it. diff --git a/docs/ADR/ADR-002-Repository-Structure.md b/docs/ADR/ADR-002-Repository-Structure.md new file mode 100644 index 0000000..dadc495 --- /dev/null +++ b/docs/ADR/ADR-002-Repository-Structure.md @@ -0,0 +1,38 @@ +# ADR-002: Repository Structure + +- **Status:** Accepted +- **Date:** 2026-07-14 + +## Context + +MEP adds a frontend, backend, and infrastructure around an existing migration +engine that already lives in this repository. We must decide whether to split +these into multiple repositories or keep a single one, and how to preserve the +existing engine. + +## Decision + +Adopt a **monorepo** with the following top-level layout: + +``` +PostgreDataMigrationApp/ +├── backend/ # FastAPI application +│ └── migration/ # Existing engine, preserved unchanged +├── frontend/ # React + TypeScript SPA +├── docs/ # Architecture, guides, ADRs +├── uploads/ # Runtime CSV uploads (gitignored) +├── docker-compose.yml # Full-stack local orchestration +└── .github/workflows/ # CI/CD +``` + +The original engine is moved verbatim into `backend/migration/` so its internal +relative paths (`build/`, `evals/`, `infra/`, `tests/`) remain valid. + +## Consequences + +- Atomic commits can span frontend, backend, and engine together. +- Single clone and one `docker compose up` for the whole stack simplifies onboarding. +- The engine is preserved without code changes, minimizing regression risk. +- A shared CI pipeline covers all components. +- Trade-off: a larger repo and coarser-grained access control than split repos, + acceptable at the current team size. diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..fbe7e32 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,59 @@ +# MEP API Reference + +Base URL (local): `http://localhost:8000` + +The API is a FastAPI service. Interactive docs are available at `/docs` +(Swagger UI) and `/redoc` when the backend is running. + +> **Status:** Phase 0 foundation. Only the root and health endpoints exist +> today; migration, evaluation, and reporting endpoints are added in later phases. + +## `GET /` + +Root route confirming the API is running. + +**Request** + +```http +GET / HTTP/1.1 +Host: localhost:8000 +``` + +**Response** — `200 OK` + +```json +{ + "message": "MEP API is running", + "version": "0.1.0" +} +``` + +## `GET /api/health` + +Service health check. Always returns `200 OK`. If the database is unreachable, +the response still succeeds but reports `"database": "disconnected"`. + +**Request** + +```http +GET /api/health HTTP/1.1 +Host: localhost:8000 +``` + +**Response** — `200 OK` + +```json +{ + "status": "healthy", + "version": "0.1.0", + "environment": "development", + "database": "connected" +} +``` + +| Field | Type | Description | +| ------------- | ------ | -------------------------------------------------- | +| `status` | string | Always `"healthy"` when the service responds. | +| `version` | string | API version. | +| `environment` | string | Value of `APP_ENV`. | +| `database` | string | `"connected"` or `"disconnected"`. | diff --git a/docs/Architecture.md b/docs/Architecture.md new file mode 100644 index 0000000..66eb518 --- /dev/null +++ b/docs/Architecture.md @@ -0,0 +1,57 @@ +# MEP Architecture + +The Migration Evaluation Platform (MEP) is a 3-tier web application that wraps +the existing CSV → PostgreSQL migration engine with a modern UI and API. + +## Overview + +``` +┌──────────────┐ HTTP/JSON ┌──────────────┐ SQL ┌──────────────┐ +│ Frontend │ ───────────────▶ │ Backend │ ──────────▶ │ PostgreSQL │ +│ React + MUI │ │ FastAPI │ │ Database │ +│ (nginx :80) │ ◀─────────────── │ (uvicorn │ ◀────────── │ (:5432) │ +│ │ responses │ :8000) │ rows │ │ +└──────────────┘ └──────────────┘ └──────────────┘ +``` + +### 1. Presentation tier — React + TypeScript (`frontend/`) +- Vite-built single-page application styled with Material UI to resemble the + Azure Portal. +- Served in production by nginx, which also proxies `/api` calls to the backend. + +### 2. Application tier — FastAPI (`backend/`) +- Python 3.11 service run with uvicorn on port 8000. +- Exposes the REST API (health today; migration/evaluation/reporting later). +- CORS enabled for local development. +- Configuration is environment-driven via `pydantic-settings` (see `backend/config.py`). + +### 3. Data tier — PostgreSQL +- `postgres:15-alpine`, provisioned by Docker Compose with a named volume for + persistence. + +## Migration engine location + +The original, battle-tested migration engine is preserved **unchanged** at +`backend/migration/`: + +- `backend/migration/build/` — SQL-first, multi-engine framework and the + CSV → PostgreSQL loader/validator pipeline. +- `backend/migration/evals/` — data-driven black-box scenarios and the runner. +- `backend/migration/infra/` — containerization and Azure Terraform IaC. +- `backend/migration/tests/` — SQL assertions and Python test modules. + +The FastAPI backend (`backend/api/`, `backend/services/`) **wraps and +orchestrates** this engine rather than reimplementing its logic. + +## Key design decisions + +- **Wrap, don't rewrite.** The proven CSV → PostgreSQL engine is retained as-is; + new value is added around it. +- **Monorepo layout.** Frontend, backend, engine, docs, and infra live in one + repository for atomic changes and simple onboarding. +- **Container-first.** The whole stack (React + FastAPI + PostgreSQL) is + reproducible via a single `docker compose up`. +- **Config via environment.** No secrets in code; all runtime config flows + through environment variables / `.env`. + +See the `docs/ADR/` directory for detailed Architecture Decision Records. diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md new file mode 100644 index 0000000..a1562be --- /dev/null +++ b/docs/DeveloperGuide.md @@ -0,0 +1,85 @@ +# MEP Developer Guide + +How to run, develop, and test the Migration Evaluation Platform locally. + +## Prerequisites + +- **Docker** and **Docker Compose** (v2, `docker compose`) +- For running the tiers outside containers: + - **Python 3.11+** + - **Node.js 20+** + +## Environment setup + +1. Copy the example environment file and adjust if needed: + + ```bash + cp .env.example .env + ``` + + | Variable | Default | Purpose | + | ------------------- | -------------------------------------------------------- | ------------------------------- | + | `DATABASE_URL` | `postgresql://mep_user:mep_password@db:5432/mep_db` | Backend → PostgreSQL connection | + | `APP_ENV` | `development` | Runtime environment | + | `DEBUG` | `true` | Verbose logging | + | `POSTGRES_DB` | `mep_db` | Database name | + | `POSTGRES_USER` | `mep_user` | Database user | + | `POSTGRES_PASSWORD` | `mep_password` | Database password | + +## Run locally (recommended — Docker Compose) + +```bash +docker compose up --build +``` + +Services: + +- Frontend → http://localhost:3000 +- Backend → http://localhost:8000 (docs at http://localhost:8000/docs) +- Postgres → localhost:5432 + +Stop with `Ctrl+C`, or `docker compose down` (add `-v` to also drop the DB volume). + +## Run tiers individually (optional) + +### Backend + +```bash +cd backend +pip install -r requirements.txt +uvicorn main:app --reload --port 8000 +``` + +### Frontend + +```bash +cd frontend +npm install +npm run dev +``` + +## Running tests + +### Backend (pytest) + +```bash +cd backend +pytest tests/ -v +``` + +### Frontend build check + +```bash +cd frontend +npm run build +``` + +## Branch strategy + +- `main` — production-ready, protected. Releases are cut from here. +- `develop` — integration branch for completed features. +- `feature/*` — one branch per feature/fix, branched from `develop`, + merged back via pull request. + +CI (GitHub Actions, `.github/workflows/ci.yml`) runs backend tests, the +frontend build, and a Docker build on every push/PR to `main` and `develop`. diff --git a/docs/Phase0-Setup-Manual.md b/docs/Phase0-Setup-Manual.md new file mode 100644 index 0000000..d00f7c4 --- /dev/null +++ b/docs/Phase0-Setup-Manual.md @@ -0,0 +1,786 @@ +# Migration Evaluation Platform (MEP) — Phase 0 Setup Manual + +> **Purpose:** This manual documents every step taken to transform the existing `PostgreDataMigrationApp` (a CLI-based CSV-to-PostgreSQL migration engine) into an enterprise-grade web application foundation. +> +> **Audience:** Developers, QA engineers, or anyone replicating this setup on a similar project. +> +> **Outcome:** A running 3-tier application (React + FastAPI + PostgreSQL) with CI/CD, documentation, and a clean Git workflow — ready for feature development. + +--- + +## Table of Contents + +1. [Prerequisites](#1-prerequisites) +2. [Step 1 — Repository Assessment](#2-step-1--repository-assessment) +3. [Step 2 — Repository Restructure](#3-step-2--repository-restructure) +4. [Step 3 — FastAPI Backend Setup](#4-step-3--fastapi-backend-setup) +5. [Step 4 — React + TypeScript Frontend Setup](#5-step-4--react--typescript-frontend-setup) +6. [Step 5 — Docker Compose Orchestration](#6-step-5--docker-compose-orchestration) +7. [Step 6 — GitHub Actions CI/CD Pipeline](#7-step-6--github-actions-cicd-pipeline) +8. [Step 7 — Documentation](#8-step-7--documentation) +9. [Step 8 — Git Commit, Push & Pull Request](#9-step-8--git-commit-push--pull-request) +10. [Post-Setup Verification](#10-post-setup-verification) +11. [Known Issues & Workarounds](#11-known-issues--workarounds) +12. [What's Next — Phase 1](#12-whats-next--phase-1) + +--- + +## 1. Prerequisites + +Before starting, ensure you have: + +| Tool | Version | Purpose | +|------|---------|---------| +| Git | 2.30+ | Version control | +| Docker | 20.10+ | Containerisation | +| Docker Compose | 2.0+ | Multi-container orchestration | +| Python | 3.11+ | Backend development | +| Node.js | 20+ | Frontend development | +| npm | 9+ | Frontend package management | +| GitHub account | — | Repository hosting | + +--- + +## 2. Step 1 — Repository Assessment + +**Goal:** Understand the existing codebase before making any changes. + +### What we assessed + +We cloned the existing repository and documented: + +```bash +git clone https://github.com/amar-python/PostgreDataMigrationApp.git +cd PostgreDataMigrationApp +``` + +### Key findings + +| Area | Details | +|------|---------| +| **Migration engine** | Bash + Python pipeline: `build/csv/validator.py` validates CSVs, `build/csv_loader.sh` routes to engine-specific loaders, `build/csv/loader_postgresql.sh` uses PostgreSQL `COPY` for high-performance loading | +| **Database support** | PostgreSQL (primary), MariaDB, SQLite, InfluxDB, Redis, Teradata | +| **Testing** | 85 SQL assertions across 5 suites, 9 Python test modules, 23 CSV edge-case eval scenarios | +| **Infrastructure** | Dockerfile (python:3.11-slim), Terraform configs for Azure, GitHub Actions CI | +| **Configuration** | Environment-variable driven via `config.local.env` (gitignored) | +| **Web framework** | None — purely CLI/script-based | + +### Output + +A detailed assessment was saved to `repo_assessment.md` for reference. + +### Why this matters + +- Prevents accidentally deleting or breaking existing functionality +- Identifies what can be reused (the migration engine) vs. what needs to be built (web layer) +- Informs the restructuring strategy + +--- + +## 3. Step 2 — Repository Restructure + +**Goal:** Reorganise the repository into an enterprise monorepo layout while preserving all existing code. + +### 2.1 Create a feature branch + +```bash +git checkout -b feature/foundation-setup +``` + +> **Rule:** Never work directly on `main`. Every change goes through a feature branch → PR → review → merge. + +### 2.2 Move existing code into `backend/migration/` + +```bash +# Preserve git history with git mv +git mv build/ backend/migration/build/ +git mv evals/ backend/migration/evals/ +git mv infra/ backend/migration/infra/ +git mv tests/ backend/migration/tests/ +``` + +> **Important:** We used `git mv` (not `mv`) to preserve file history in Git. + +### 2.3 Create the new directory structure + +```bash +# Backend directories +mkdir -p backend/api/routes +mkdir -p backend/services +mkdir -p backend/evaluation +mkdir -p backend/reports +mkdir -p backend/database + +# Frontend, Docker, Docs, Uploads +mkdir -p frontend +mkdir -p docker +mkdir -p docs/ADR +mkdir -p uploads +``` + +### 2.4 Add `.gitkeep` files + +Empty directories are not tracked by Git. Place a `.gitkeep` file in each: + +```bash +touch frontend/.gitkeep +touch docker/.gitkeep +touch uploads/.gitkeep +touch backend/services/.gitkeep +touch backend/evaluation/.gitkeep +touch backend/reports/.gitkeep +touch backend/database/.gitkeep +``` + +### 2.5 Create `backend/migration/README.md` + +Document that this directory contains the original engine: + +```markdown +# Original Migration Engine + +This directory contains the original PostgreDataMigrationApp codebase, +preserved and relocated here as part of the MEP v2 restructure. + +The FastAPI backend (backend/api/, backend/services/) wraps and +orchestrates this engine rather than reimplementing it. +``` + +### 2.6 Update root `README.md` + +Update to introduce MEP v2 with a link to the original engine at `backend/migration/`. + +### 2.7 Update `.gitignore` + +Add entries for: + +```gitignore +# Frontend +node_modules/ +frontend/node_modules/ +frontend/build/ +frontend/dist/ + +# Python +__pycache__/ +*.py[cod] +.env +.env.* + +# Uploads +uploads/* +!uploads/.gitkeep +``` + +### Final directory structure after this step + +``` +PostgreDataMigrationApp/ +├── backend/ +│ ├── api/ ← new (empty placeholder) +│ ├── services/ ← new (empty placeholder) +│ ├── migration/ ← existing code moved here +│ │ ├── build/ +│ │ ├── evals/ +│ │ ├── infra/ +│ │ └── tests/ +│ ├── evaluation/ ← new (empty placeholder) +│ ├── reports/ ← new (empty placeholder) +│ └── database/ ← new (empty placeholder) +├── frontend/ ← new (empty placeholder) +├── docker/ ← new (empty placeholder) +├── docs/ ← new (empty placeholder) +├── uploads/ ← new (empty placeholder) +├── .github/workflows/ ← existing (preserved) +├── .gitignore ← updated +└── README.md ← updated +``` + +--- + +## 4. Step 3 — FastAPI Backend Setup + +**Goal:** Create a working API server with a health endpoint that proves the stack is functional. + +### 3.1 Create the backend files + +#### `backend/config.py` — Application settings + +```python +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + DATABASE_URL: str = "postgresql://mep_user:mep_password@db:5432/mep_db" + APP_ENV: str = "development" + DEBUG: bool = True + + class Config: + env_file = ".env" + +settings = Settings() +``` + +#### `backend/database/connection.py` — Database layer + +```python +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker +from config import settings + +engine = create_engine(settings.DATABASE_URL, pool_pre_ping=True) +SessionLocal = sessionmaker(bind=engine) + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +def check_db_connection() -> bool: + try: + with engine.connect() as conn: + conn.execute(text("SELECT 1")) + return True + except Exception: + return False +``` + +#### `backend/database/models.py` — Base ORM model + +```python +from sqlalchemy.orm import DeclarativeBase + +class Base(DeclarativeBase): + pass +``` + +#### `backend/api/routes/health.py` — Health check endpoint + +```python +from fastapi import APIRouter +from config import settings +from database.connection import check_db_connection + +router = APIRouter() + +@router.get("/health") +def health_check(): + return { + "status": "healthy", + "version": "0.1.0", + "environment": settings.APP_ENV, + "database": "connected" if check_db_connection() else "disconnected" + } +``` + +#### `backend/main.py` — Application entrypoint + +```python +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from api.routes.health import router as health_router + +app = FastAPI(title="Migration Evaluation Platform", version="0.1.0") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(health_router, prefix="/api") + +@app.get("/") +def root(): + return {"message": "MEP API is running", "version": "0.1.0"} +``` + +### 3.2 Create `backend/requirements.txt` + +``` +fastapi>=0.110.0 +uvicorn[standard]>=0.29.0 +sqlalchemy>=2.0.0 +alembic>=1.13.0 +pydantic-settings>=2.0.0 +python-multipart +python-dotenv +psycopg2-binary +pytest +httpx +pytest-asyncio +``` + +### 3.3 Create `backend/Dockerfile` + +```dockerfile +FROM python:3.11-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +EXPOSE 8000 +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] +``` + +### 3.4 Create backend tests + +#### `backend/tests/test_health.py` + +```python +from fastapi.testclient import TestClient +from main import app + +client = TestClient(app) + +def test_root(): + response = client.get("/") + assert response.status_code == 200 + assert "version" in response.json() + +def test_health(): + response = client.get("/api/health") + assert response.status_code == 200 + assert response.json()["status"] == "healthy" +``` + +### 3.5 Verify + +```bash +cd backend +pip install -r requirements.txt +pytest tests/ -v +``` + +Expected output: +``` +tests/test_health.py::test_root PASSED +tests/test_health.py::test_health PASSED +``` + +--- + +## 5. Step 4 — React + TypeScript Frontend Setup + +**Goal:** Create a professional UI shell that looks like Azure Portal — enterprise-grade navigation with no business logic yet. + +### 4.1 Scaffold the project + +```bash +cd frontend +npm create vite@latest . -- --template react-ts +npm install +``` + +### 4.2 Install dependencies + +```bash +npm install @mui/material @emotion/react @emotion/styled @mui/icons-material +npm install react-router-dom +npm install axios +npm install @fontsource/roboto +``` + +### 4.3 Create the application structure + +``` +frontend/src/ +├── main.tsx ← Entry point (ThemeProvider + BrowserRouter) +├── App.tsx ← Top-level routing +├── theme.ts ← MUI theme (Azure Blue #0078D4) +├── components/ +│ └── Layout/ +│ ├── AppLayout.tsx ← Sidebar + TopBar + content area +│ ├── Sidebar.tsx ← Collapsible nav drawer +│ └── TopBar.tsx ← App bar with API status indicator +├── pages/ +│ ├── Dashboard.tsx ← Placeholder page +│ ├── MigrationRuns.tsx ← Placeholder page +│ ├── NewMigration.tsx ← Placeholder page +│ ├── Validation.tsx ← Placeholder page +│ ├── Reports.tsx ← Placeholder page +│ ├── History.tsx ← Placeholder page +│ └── Administration.tsx ← Placeholder page +└── api/ + └── client.ts ← Axios instance → http://localhost:8000 +``` + +### 4.4 Key design decisions + +| Decision | Detail | +|----------|--------| +| **Primary colour** | `#0078D4` (Azure Blue) | +| **Typography** | Segoe UI / Roboto | +| **Sidebar** | Dark background, collapsible to icon-only rail | +| **Navigation items** | Dashboard, Migration Runs, New Migration, Validation, Reports, History, Administration | +| **API status** | Green/red dot in TopBar — pings `GET /api/health` on load | +| **Routing** | React Router v6 with nested routes | + +### 4.5 Configure Vite proxy + +#### `frontend/vite.config.ts` + +```typescript +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + } + } + } +}) +``` + +### 4.6 Create `frontend/Dockerfile` (multi-stage) + +```dockerfile +# Build stage +FROM node:20-alpine AS build +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +# Serve stage +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +``` + +### 4.7 Create `frontend/nginx.conf` + +```nginx +server { + listen 80; + root /usr/share/nginx/html; + index index.html; + + location /api { + proxy_pass http://backend:8000; + } + + location / { + try_files $uri $uri/ /index.html; + } +} +``` + +### 4.8 Verify + +```bash +cd frontend +npm run build +``` + +Expected: Build completes successfully with no errors. + +--- + +## 6. Step 5 — Docker Compose Orchestration + +**Goal:** One command (`docker compose up`) starts the entire application. + +### 5.1 Create `docker-compose.yml` (repo root) + +```yaml +version: '3.8' + +services: + db: + image: postgres:15-alpine + environment: + POSTGRES_DB: mep_db + POSTGRES_USER: mep_user + POSTGRES_PASSWORD: mep_password + ports: + - "5432:5432" + volumes: + - mep_pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U mep_user -d mep_db"] + interval: 5s + timeout: 5s + retries: 5 + + backend: + build: ./backend + ports: + - "8000:8000" + environment: + DATABASE_URL: postgresql://mep_user:mep_password@db:5432/mep_db + APP_ENV: development + depends_on: + db: + condition: service_healthy + volumes: + - ./uploads:/app/uploads + + frontend: + build: ./frontend + ports: + - "3000:80" + depends_on: + - backend + +volumes: + mep_pgdata: +``` + +### 5.2 Create `.env.example` (repo root) + +```env +DATABASE_URL=postgresql://mep_user:mep_password@db:5432/mep_db +APP_ENV=development +DEBUG=true +POSTGRES_DB=mep_db +POSTGRES_USER=mep_user +POSTGRES_PASSWORD=mep_password +``` + +### 5.3 Verify + +```bash +docker compose up --build +``` + +Expected results: +- PostgreSQL starts and passes health check +- FastAPI starts on port 8000 +- React/Nginx starts on port 3000 +- Visit `http://localhost:3000` → see the MEP UI shell +- Visit `http://localhost:8000/api/health` → see health JSON + +--- + +## 7. Step 6 — GitHub Actions CI/CD Pipeline + +**Goal:** Automated testing on every push and pull request. + +### 6.1 Create `.github/workflows/ci.yml` + +```yaml +name: MEP CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + backend-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - run: pip install -r backend/requirements.txt + - run: pytest backend/tests/ -v + + frontend-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - run: cd frontend && npm ci && npm run build + + docker-build: + runs-on: ubuntu-latest + needs: [backend-test, frontend-build] + steps: + - uses: actions/checkout@v4 + - run: docker compose build +``` + +### 6.2 Pipeline flow + +``` +Push / PR + ├── backend-test → Python 3.11 → pytest + ├── frontend-build → Node 20 → npm build + └── docker-build → docker compose build (runs after both pass) +``` + +> **Note:** If using the Abacus AI GitHub App to push, the `workflows` permission may not be available. In that case, the CI file should be placed at `docs/ci/ci.yml.pending` and manually moved to `.github/workflows/ci.yml` via the GitHub web UI (see [Known Issues](#11-known-issues--workarounds)). + +--- + +## 8. Step 7 — Documentation + +**Goal:** Professional engineering documentation that demonstrates architectural thinking. + +### 7.1 Files created + +| File | Purpose | +|------|---------| +| `docs/Architecture.md` | 3-tier architecture overview (React → FastAPI → PostgreSQL), existing engine location, design decisions | +| `docs/DeveloperGuide.md` | How to run locally, prerequisites, environment setup, branch strategy, how to run tests | +| `docs/API.md` | Current API endpoints (`GET /` and `GET /api/health`) with request/response examples | +| `docs/ADR/ADR-001-Technology-Choices.md` | Why React + MUI, FastAPI, PostgreSQL were chosen | +| `docs/ADR/ADR-002-Repository-Structure.md` | Why monorepo layout was chosen, how existing engine was preserved | + +### 7.2 Documentation conventions + +- **Architecture Decision Records (ADRs):** Numbered sequentially (ADR-001, ADR-002, ...). Each new major decision gets its own ADR. +- **Keep documentation alongside code:** All docs live in the `docs/` directory within the repo. +- **Update as you go:** Each phase should add/update relevant docs. + +--- + +## 9. Step 8 — Git Commit, Push & Pull Request + +**Goal:** Get all changes onto GitHub with a proper PR workflow. + +### 8.1 Stage and commit + +```bash +git add -A +git commit -m "feat(foundation): Phase 0 complete — MEP project foundation + +- Restructured repo: existing engine preserved in backend/migration/ +- FastAPI backend skeleton with health endpoint +- React + TypeScript frontend shell with Azure Portal UI +- Docker Compose stack (React + FastAPI + PostgreSQL) +- GitHub Actions CI/CD pipeline +- Documentation (Architecture, DeveloperGuide, API, ADRs)" +``` + +### 8.2 Push the branch + +```bash +git push -u origin feature/foundation-setup +``` + +### 8.3 Create a Pull Request + +On GitHub, create a PR: +- **From:** `feature/foundation-setup` +- **To:** `main` +- **Title:** `feat(foundation): Phase 0 — MEP project foundation` +- **Description:** Include what's in the PR, acceptance criteria, and any manual steps needed + +### 8.4 Review and merge + +> **Rule:** Never merge your own PRs without review. On a team, another developer would review. For a solo project, at minimum review the diff yourself on GitHub before merging. + +--- + +## 10. Post-Setup Verification + +After merging, verify everything works: + +### Checklist + +- [ ] `git clone` the repo fresh → all files present +- [ ] `docker compose up --build` → all 3 services start +- [ ] `http://localhost:3000` → frontend loads with navigation +- [ ] `http://localhost:8000` → API root responds with version +- [ ] `http://localhost:8000/api/health` → health check with DB status +- [ ] `pytest backend/tests/ -v` → 2 tests pass +- [ ] `cd frontend && npm run build` → build succeeds +- [ ] GitHub Actions CI triggers on push/PR and passes + +--- + +## 11. Known Issues & Workarounds + +### Issue 1: GitHub App cannot push workflow files + +**Problem:** The Abacus AI GitHub App (or similar CI tools) may lack the `workflows` permission, causing a 403 error when pushing `.github/workflows/ci.yml`. + +**Error message:** +``` +remote rejected — refusing to allow a GitHub App to create or update workflow +without `workflows` permission +``` + +**Workaround:** +1. Place the CI file at `docs/ci/ci.yml.pending` in your commit +2. Push the branch (this will succeed) +3. Manually create `.github/workflows/ci.yml` via the GitHub web UI: + - Navigate to your branch on GitHub + - Click **Add file → Create new file** + - Name: `.github/workflows/ci.yml` + - Paste the CI content + - Commit directly to the branch + +### Issue 2: Database shows "disconnected" in health check + +**Expected behaviour** when running the backend without Docker Compose. The health endpoint returns 200 regardless — it reports DB status but doesn't fail if the DB is unreachable. Run `docker compose up` to start PostgreSQL alongside the backend. + +--- + +## 12. What's Next — Phase 1 + +Phase 0 provides the **foundation**. No business logic exists yet. + +### Phase 1: CSV Upload (Complete Vertical Slice) + +The next phase will deliver: + +| Layer | Feature | +|-------|---------| +| **Frontend** | Drag-and-drop file upload, multi-file support (10+ CSVs), progress indicators, file removal | +| **Backend** | Upload API endpoint, file storage, metadata persistence, validation | +| **Database** | `MigrationRun` and `UploadedFile` tables | +| **Tests** | API tests, UI tests (Playwright), integration tests | +| **CI** | Extended pipeline with migration-specific tests | + +**Acceptance Criteria for Phase 1:** +A user can create a migration run, upload 10 CSV files, see file metadata, delete files, and refresh the page without losing uploaded file information. + +### Remaining Phases + +| Phase | Feature | Tag | +|-------|---------|-----| +| 2 | Schema Discovery | v0.2 | +| 3 | Mapping Engine | v0.3 | +| 4 | Validation Engine | v0.4 | +| 5 | Migration Engine (wrap existing code) | v0.5 | +| 6 | Evaluation Engine | v0.6 | +| 7 | Reports (PDF/HTML/JSON) | v0.7 | +| 8 | Dashboard & History | v1.0 | + +--- + +## Appendix: Git Branch Strategy + +``` +main ← production-ready, tagged releases + │ + ├── develop ← integration branch + │ │ + │ ├── feature/foundation-setup (Phase 0) + │ ├── feature/upload-ui (Phase 1) + │ ├── feature/upload-api (Phase 1) + │ ├── feature/schema-engine (Phase 2) + │ ├── feature/mapping-engine (Phase 3) + │ ├── feature/evaluation-engine (Phase 6) + │ └── feature/dashboard (Phase 8) +``` + +**Every feature:** +- Has its own branch +- Has its own pull request +- Passes CI +- Is merged only after review + +--- + +*Document generated: 15 July 2026* +*Project: Migration Evaluation Platform (MEP) v0.1.0* +*Repository: https://github.com/amar-python/PostgreDataMigrationApp* diff --git a/docs/ci/ci.yml.pending b/docs/ci/ci.yml.pending new file mode 100644 index 0000000..109c8b3 --- /dev/null +++ b/docs/ci/ci.yml.pending @@ -0,0 +1,61 @@ +name: MEP CI/CD + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + backend-test: + name: Backend Tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + + - name: Install dependencies + run: pip install -r backend/requirements.txt + + - name: Run pytest + working-directory: backend + run: pytest tests/ -v + + frontend-build: + name: Frontend Build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node 20 + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + working-directory: frontend + run: npm ci + + - name: Build + working-directory: frontend + run: npm run build + + docker-build: + name: Docker Build + runs-on: ubuntu-latest + needs: [backend-test, frontend-build] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build images + run: docker compose build diff --git a/frontend/.abacus.donotdelete b/frontend/.abacus.donotdelete new file mode 100644 index 0000000..204d229 --- /dev/null +++ b/frontend/.abacus.donotdelete @@ -0,0 +1 @@ +gAAAAABqVijPkE_IN3BxmFw0_JWUt9q5GwApRWu1r_S-g3aguTH0N3YF8lRCZZ_u1LRqSBVbvVsjCfj_m-gDJwtj77nnitPftbzWJKc4YcM0-V1VH2OL9XuU26F_NwhJgJ-KycglH2aI1VLtA9VpFZaceO5kWeqaTZU2XMGbI6e5wywbFmRY9UkE5IsA6dPa4M4NpGLaTsYjFkQoNDc5FpHA7-CAETtd12lVuvczEcry-m0epLvE_NPSDw9FsEmWVtxKptnm2ER0cJ-nrX6a0g75TbXApbzI0VB_ecp4Hjrwiibbkk1rwq_X49nVEvwCXJJhZzeEAz549eHWL6viOtPWZwWSuVxlE3nf-hlclyB9thYHkFglR9_Lx_D08s1ukgkyXLwnWnl10AXI3yU35YEGv8JBeIcm8FVXB5PZQ9QuxYfht48qU-v2eaYEwMtw5zq1zUaSb4plRX5FVutZy1R_mgTjXqGruw2NXwUr2EzZK5r-cTMp05g1pumxlB0W_OMZgHUMeZz3diCKHVypFndoX0sIldo_XHDOo0lXoxiRkLfvqh3q7o4QqKzzl81KCsAo0BCoeaNYgv3YSiVGS_CB14yroi4MVUCPiS6Lj5LMceIsER2ha9gJXk26P06PyhZhRRsp8sZLbgkDKI0KctvM1g4VUMJsKIJzC8blQadfxfS0tJ5IBvUgj6HIYEW8TI8FMaRm9VVF-g64xaBvjhJEC6yaQfBQqfiD853j72ZXN7Bieu256UGr8x41o_T9It17B3aJLUIgFbKQwbxRVxMJ4prR5XIai_ITqorhg952M_i5hOytMQSSsJhEUR5rrF9BE3GsUK5fAFC625eclsBWCHJdF-qPnKCQ3lu2rKO8KcGHU8DC-AO_jei-gFp5MbWUJUPXwqbl591CZixj7lz-5k6ZUcVjBwyiyVgtXpB4eKzQrMV_hPN6MkTsdQL4aSVyP84-QcBtwuh3YOjFmclmC55OUimdjBEXelrekZ_07bDhrYDA-F4vMpNldJQf7rW4UB988LN8QBZ0G-4pZVVjqt9_DkloO38qJyEXgA8y3m27h_2aIof7GnnK4QHbAafKbGHFoUpzmOgCmjgks339R2WizMp5Pgm3nsHRZRgTjKtKDop8YEo7QtSjJIrJ9-n-DW0ru3jpZOWCw_6IipRld_NJTRgg_h7ahLmGW21siIqToSXUotW2RLMoY4fNGZ5EBFoeFGPPR1GJDVH0DUCqv_0I5rGARGb-js91ytqgV3UFrbaGJQYW-LgAVeSdVnWY_UX8WUR_M3MWUnF-hEeoets08lIsFW4mJSx6xgAdySVa3ViBloiUDp4mTT17pngyO9l_W3gz-DMJOih7KzooaQfkbPRTxOiSSg5-OdTKyNy0XJFv3Ex_03nf8VUT3SKJZPe8YTmor89Njng1qHQFbBwlVU6PqNOgqQEWy6RHYNg_kEiFE87RFcrvJyoS_qcd6-uZaqkHKP5qhXq105UOv02oeV3d25ZRkuuRhji8BWG3yTFERxKXK2Y-OXIccJRobRoqS_4-JMpRFtHhZ0COcdaTML7iRHMu0SsW25TgP-6-h4g2sO9dSfu72jyzbtNU764W84NVDXscSbFbg2tFQgqU3SKCfm3WPMg8sCqlUAzwfgJuLw2NBJ1y9UPsUKe6FG0bmdDR8-GPbPjCuIhXKyF1ciQsUJL4Y687gp9jzv5L6z3wMwYmUExdA-sYEJxwnSMspH_-sxyPy5HeshUoa8oguMG1Gwh5Ej95bWznG82xIz7IPrvc_hkylAI6XqtdMUOzcBlRvfLwhtRHSUisfne-aTTfb96Dax4gvddhcrQfN8LVeWM4LaZ68DgeowbviGDICfFlgouCOm-F2PNOAOefxeoFN8ogQPsCOz7OBZYKP5vLj5vDRoqoLSrI3UiXmfRlE6PCuKxsnZTUJfwsiOG0o_NZJLT-eQsStEWxOlBHmWMazSG2cnrpseIvDWgdjNiGS1esx7iV0pCJ0774Xj5D4uM8kSNQloy4DREhN9YZWOXbq39D03fs-uNV-6pcyA-HQPhVJENICsNHjJ9gCrhr9whVC4vDzTbS7DuxkJ1Xe7SqiJTh5FdAs4WmX0zzGvKliuYN70kdeb7QLbUsywErm1c68U72vrHiQy0ghzK2Ul7P_FagqEHBs_CRCBpQSSU00oPKxfze5s2vNEVmMSZ1KgLuOol5OJ33IV_dAQ6otcDqvfEpgTNqTASgme0TyHazPC-vhnI7l63Nx5OU_tUy9QM5lXU_LZf6FjHYeAvfKKUjKD2eAK4PGyKo7LgEqtl4ZzV_4qPMHwG8zMObRFNz17Z6Tx0qGAWW9uKIXoXfHaspNdMSLmFx5akPiLKj95ZrGyJYuy8vwYy10fRYPqkmTw1Z5cQRSJZiR0coKiaDTRsGOdlhQKFrD-ODEbCt78NQj8XGCwQAyVgD0hkKtG85FBGhYYrd0byhynldZ3Zvzy5wDwnPsCIzXTkGb0ql2-slWbwiaBhqe4I4b7YFplSwhNf04UkW-L1xYmayKIdvp311dZ7kqoTD1eVc-F70I_r9EU-Ec048NTsV4GvKUAiF4Y0R98YbgsT6yYJ-FEmdcxCt1yN275p1rJxtAYvAXjfxrzWcc2N3lpUq0eLD2zlWq-YbqEh8yO1eMiXbJ5j3A5h-pSjnn6dznrYhP7g-QbRyrbJhhobsoXxMnfeMq5JEbpw1gOgPWLYSf4r1y7buXgMa5SyHgIeTYynJ8vdwDeFciPLJTmjHF26PzVu_IpkUgwy-18-ob_QOI-4r8nruTJWIBroX8j_tM-qdedHterOOHAao_cTZUtN0ce0Os347JwOCXOL0uuz1DIi4fVB4-SsJRR9JKpPkyqRnkYPoYf5sM3nUzE8WkccuthO0DXzfS3AbbQ6hOFZfjdBHf4HTQMlNhsQe-p5S4HKO9m-PKKyOXVNHLV05c8Nm3j7CMZaEHSpFBpUzxpKoL5ZlcW7wpZesZoGHpubsaa8UWtHa-JOO950bK6e3tVnVBzwFr8kgJqO6NOpPNPicVkGkgoeZgxe5XAVveO8d2hCFhhMOPRnKxyVeqdl9kugKcmapF5M3zryxW5zNWz61AaLAwWr-WYLJSlDStuFhyPSmfnaCnkbbj8e1KuqY7JxQw98YQpUdAA8bK5c5gaKWYHAM7iOQSDUv2DGMiRRS5t0NUzWJ1Q535AEsS1xtvclSBubTLbdhabPdZAETk1oE8SvQdd0VqIjStYVgdB_OfWdej8lG2IEk5sPRYLhCIjkdmfKA0ySZnIWCx2qTrSCsre0awYmhC-ye3ENxBVD2fLrqxLyZ7lRFh_H4YcWjhKiQtLEAluoo-dGqpy-r-OXbi79Pu7pGLgpY6kBEHFvuBMb10TcPx2lMqjUy7hIeeIoGv5S9WOs-oZB90uJP5PvhgaQzrmgDAmXB4km6R9DULtL2_BkwuG8CbAbsJspZsBivyy-WT_c_nOb6Zvr9ZkKSIEJKDVJwkLKRES7FwEq0I8MC0YFUu2qSKDs-1_XQ3HJF-ySi4ZpIk500f3mVARVEo_3eyINbx5E2hb9HQ6LyjBkhps6pjOsutzFJMr2klklx7v4Zs8TVuXXzoqI9tGeK9xU4Inwwk8B_MGfImyAhiruwaRI_RvXgDJJez8oY2Qwnv7r_jgWXBWqbBeUo8j16By9sPBsyIM-6UhC_OjJ2RYjJkzYwXSDm9tiwRf_Akvon5WF-tpKCoagS08RjszsRNXa_MYD139oGBj4BqWswMSpikBrShzNqyJvAZrDT5pU4RQsMMqJo6vGQzownASSNhxxT1oPF2zkYWtbATspn2vO7YRC3fWxRUKprUvrY9k1Kcqub4gvnBuMC4JkbmcCkbhBVWJv-PUG86QQBKkNyl4eTpsGq3myhsU2b3NWDVNt-O6tmTZ9MHMdn-kJlK4NjT8LPstpXEwm0f6bk5dNNrNu2aHSmDHdVRgfkvWKicYrDyfQu5VaZM8iwbYnB_BjzLV8IzyKdhNadaS1aM22aKPoN1YzXC75Bx6V7Jc3HlP8SZhtfLwZUNfuj58HTWkv-vRKUDzqrt5KX9Y3cBks1bVgRUoIR6C7H-FLGu7pXYvLGE7E048AtHJJbROU0PV4GoAsNmd5Uw55CWhU640p6l17sHzXw3nIaeGhciDO3O4lLM_Clb8e3Vv2Wr1LhS9_A5kqiz7G3Gtumo1MnEXPg7v38-Myb6mqE0Kn-MXbwSP2ozUSooxa_1tv5nBIe4kI7j3ewMF11BAOxwG9o_BlQiOKlW0Gpnr_mkSy6fEazK3oxsGh4_mhOmI1zcGSXMLpz0bCYjalhsC9mfX6kH4lyl_dcCyeYWY4Zna1aWe0Csq0bv8biEpYIe-EMvuLv8m38hvLRN_CH9ToKgxQcvTIFvB5fPtijDb7PdexSNS8ymQwv_MMhgReAtPThv8Kn6hSEkh75unOXA9kvoivVw34NPy7nwpGTPyM_SaMX1o8fnCy9GVnj9AhsGocnyA8LI7euiWiDM9yMcM0ZPEpnbQwT5QDcLTkoyQeQCNgjbKF-nQaXGEJ1pktBFV_UKZmYy6R283jcs_ei3ORB6pBIC3mLaZBYS9MVZcf4TcWrzypo51FDLm0nfk9MfpAb-U2DgBmmOZ7hSALVYLPsPsRIFbbb0ZZqff8MAt2DW15iRkMcRWNbeNJY8Dllw-2cdJvTEIC6-iHyStfxJtcMYdW-nHlXTPmpRw509lZZaXkuxcg_snZKCj_AkuX0TTKKgI8TtYagaHOvWeT7Dy5CEmYNSJTob5ZJRPSjBLuMmRs_hJDd9rluRZJOLsbOQcmWG_kI5ht-KpFJAitdhkg8Xt_MEj3A3p3OysJBvJZKGfMRDY-piMM6QPlOHtWNUpZJcajAXeqvHGJUrSS5MxrC06ts6uwDX9YHyngbvNUh-0evT77dlp2HMB5aUhSvR9jpps-Y56ed84jZOlbVmtTvFaGtSv935S9LBL6kaYo8NO_vAasV5vZWEI6InTUklILFJG-qpBKJ6sdCl1FCavr_Cq0j_jWxOv5w7Shn5e0epMwnxNmdTy5EsdaVJbBCLJ2pqq1BCCr6D5h_x0o1Poo9GWsOOf4T3MxzXQQc1lsMWj-7xSaqrOEJjlQZP8zggUKgkYqilXb8Aqhpd1ynWMNwSIKFCL-pjIIa8Hoc6FuBwSZEGh_PwLPWWcF6jPncQhwAd0wPUxoi0Y7c_cmIw5xI6fBzHqqXvSF1r4GTVJtxZI9btbnAwnthyOUrWGyHglx9yQhboN9kFCRgchTHgKJoJoPsdNJoWLXK3yd0BECoslnPMx15JMbwx0pZ5tWsVLAJSgql-Rd0gHcNzjUWVQflDNCeXlqM3uzoTDjeoNgSiV3tm6ZDPA0tnS67FR-OEXkYW-qeCilS_e9GWyUf19f-sx3k7quYrmZJJLzYwsV28saO1SZMErJnLKeu0_ChfttLkGEqpOBJvE2BP78Ts12jbm1V_rqTMLjBYC3KXEeSuJgnEKPCI0TujcrkzDqez94GjV52jHnNYP3allsOYVE0fgMGglSfPPajdNAPk7Vjs02SD9oi_erBXne96gEM0FihB51j5ywbBRbpNq5L9KDLRm1E-hunhELZUxq2rl3PFF37ZAvXlxsdva9aNiPWBxH9jMS71wdu_6Sm7d8Ak8xLFWd4Cz72jLTghQNjaXnLIBbh7NTmmEv8aaa3y9q0Xbk0nSDzG-k4EqaSVMqhSc2_C2zZ4qvzafKUPayL6iD3VywVtI14-d9IMv-lJT4pOMFit0FOT93Kj1kkymd4brBgk2Qwxv2EnJTqHusTVpVJcSpxT41dV3vpqeM0-u6hMdnjgSv5DT985xUnX_NHKml8aFYQPaa5JfBMvzBSH_haJBXnZHbVt97uVPJJAfWnb3mttuCNr4luh2qw3ceNjBTrNqIIBWM5O35AyHuWob83XqrwwQtdoSrQYF1YKI7hBSgmo_vygQ-HZDw1quf-f3DK83u7ZfoD6wcHbeTpr3bVoplN2g0hldhViHaAPHpjBAcoyRF98onDsVO8V1asxvL33Di4OaCfwFJVfDwVplCNKnUXoZgjhPXEBamY3fDxDnzC3onvHxNStti4UwfLEFCTFBZuoufa0Tro7G7AGe8sk6042mOFeC-0igNkQgvjyax7FZej2itZsitVGBDldm_ai4uXMXeuxECMOvhw8KGgE4hIELGKvinuSpZs7voc4gtAYAitPgarjs0WXI3t6mAUcvmnPkBEGP5DdadgOE2cvs089_mvXzaTqt-qEkyd4-rc3Ac6sUBP-2rk_7HUxEpxF33kN4ACQPigZsZRwt9DvSM_SZ22n5o3tDZeDDJdEqhIbSwh4oB7VFgB939mTCbpPxhSpdngwGcpf2zqtyp0u07DrLCKaiCK9SvY7WpnK0Ei9FP_ZU6HnAW2h8sg-cNBI8kJdK1Buh7onbF6yx-cdHd4Pfj7iS4VIGwfKZuHSVEbcCO8I27Zil9ANKALVjcGkC49x45E1VciE6CexI7h08GE9d6pt1S-FGO_miA1SXFI6zyhlMg2lh189vCMbIUdaNWFF3pRAgqcNbIgH8SI2gP1gNOQS93SQXM29Xdkubx22L-xQR3ptAH5KG7AwvKCFNrxQDQBAMDZk26l9X4YFlj9qSxUn52bRECNREBIZ8k7rTI6b0wXgEwSl-g3-ep6j_gxwoRekrdlyoHEWTWthQL_JYMBGJBY96-PAJn6Z3NM_jT3NomrMomtjYvNzRaIEo_KvLpvV7fvycEAOsfevx7vHFX5joXKFx_LBjzjJ9A7fl7PgCPxuvq3b55yiuVxLZ9w1i0yR-MkU0UareLOoLnxTf0RCUm_dOnbQ3Hq5wUnlIUsCO4Y125czqjQTat3a_waEh3yjDix4asMUij-7sCpkRT0vYN1-WA7l4yy1S4P8Yr5JcV61Qf6zUjQWII64BcLJu3IDIhY9V17eQ5Z4C3NRlvGXgqK4qR3bf_BcJgZtfiFGoLyd6zovQccqKafyxclX6Acgq5a9CiV-1GGTZNj5nO78KUqbQLEprt1jKgkg5d3ukC9jqUiwhIY0yT1Zo5QxfU8rLJyg6Rj-MZyZW7F6-1Gl61eIcfV4dTLZZaP1kl6fNQ9mTXJvg3eCrpOsLwtOt2V-KW6JvAxy8LiWSVyEpkKIMnT70BvIzvTkEzgxzLbw3VkjMN8dDvJ8zsEeBJX2sZvlYRA-6G57gJZ5W5jSyhXvYHeYUsVDWvIjA7mtM04tieIW_8-65k3sD1sDeiW7H_AXipNNMkC_yluLN1Q_uVebXw1W9e3iiAdpgixVsad5CBD1wT4LtmM5Tc1kqEhRjzUZyuGd5vEajNovEWg2TP6TpO_GUgxuWqaLy6E5RtI0dRIWYqbfGCJbF8M5vdaFhiDMRH0kF1aIIrvCrC5U3GFz1uydYzTZDXw0h0eWrSWSFeO0kovG_4F1p2RA6nO6l1b4B-DN17cMWWpxUbd06VBqL6yMLm-IQ2WBlICxDTupEjaSXjpOuDSuYEVhu188wVoEf7GBIsE3yt5I_sk7I1vW0Pa_bzR4ouiKOqDqLP_blwHDoPl0JyRzmKLRVQhJ-OOixon3Vhx2bQ9IgVrKhJ8Kva3dEdyaVGvtNoRVfqikPgeZ6Y5Xc8ftWinVRFfIJXSklI7WlguqNPYlvbCVTAlnBCatY_r_5CJPjJ5iJLnir5WFFO3CKxMBswpa6rmQyfnsXv7UMyx7cXCSjUZO4XBHntWawvFCI1hvWUWu3X2mmyFNZI8U9vLGr1Hnf1Mm9Hw_WqzzPRO5O41fKFyzXdfbpQVugKB5xHl4jgMEDyB0xfsxUIb5HBXSPjsSeXyc_8nCVjNnXlMfKLejcxpy3718I9ibl0f8HquAn8KEA38JfbySui7zhissmNS172SAvkOhe_NMGb_VKlKHUvxLGVgTCFjQBe4RlcysQn1YvJQuWbLkjlAGaYN6Q16pTZ3p3W1vF-Df3BZUYN0AZeOtVlh2u04ZvUWPIu1mNxIdFBoUxhVI1eLCx6Ngu9jd6Cny9lf56nnE-12ovd_7kRFQJyhwT9NN8q0H14VFTSqtN_8SLmy0bpYvyKpfNSmc0BeXIctKbWgpHEG5gC7ubNkIPUXWmwtw1EMo0Vvf6LwwgOOzl2VDNW0JJkm3EzEXEtESca8MWNX41kX10RIMW2e0-Zxsrj4ZjR8ESHPjZURcmXeO6RRSAIOn4Pad2jOpzZYvPn-wBefGwyqfUO14PlGkhqKRSVB8O3xA1cr5O8oKzJSkwAXOk79Icd3gujsBSgNF7Wm5kNUgFSYp2HLb54wNAwJO8-kFNY0CE9QZyceU8TLYKZbbnXZi7da_t1VZNTYBmLs_bEaFRGn4b9ISUSK1Dc7CEKlL1p22CbdfHmdsPnLIvvnz0SOxQO_7qAzNWGO5qxK6c_h2c7-AZsm7gOQjZ9s79OVSPA_o1GkkGhMV9lwQk-aomIZxFO_f54s3J-7TLYMN8C3JfYoH4xoQTjxIEykzGxCFovDsGouhcSSKBgl5WJx8Dpk9BuYek-EmTg3H1UWVFA6VUnBJqjNz_wcxA_g-tzbB6NxjqEVqpPP93xiUNB4Z6SoGcEhSxA1B5AhooB1Xk41gFFhjfitdXqGKiR3svqRmaxZ4A20ol-rq2zH-ZzBUU0kKPIq_g_kdgAyDdhzR6otSaR1uy_xMW8HMCWX8BXnQRXXj8R9onwJq8od3486kBYYwkabaazLYrzkyVBW-xnauE8XrpwcBupYDYs03KE2C3j-JYlA2LoVmCY2ZZHXkCRa5d0CD7ZPa1UQIPspKai3Fw_imrj4QaJI1HUkV8vFZVnN44XRnZ_n7PQ9V312iYN6cw3S5-Rl3DJol97e3180PBe7dp_vFLOGrmivgt2935-9KzNJ-J8xN0rQrNtK_HDCZF5Rb0qAgsTcedOobtkkzeL6QxXnknU4WEcAAJr4_REm8nWymPj1N5NdGmphmyZ1OLV4O3r9l5S1831lE5SxpzYi6iORYc9Lg4C-hLKx0L41fqBxJTmd2joQOCa_xjo9-wIv4_m6COMjtNDtgR4yaHaJhIhVPEIKc7TxumsFxBShMVwExCupBsL_6ZpPuC_YJu2oihMaYSHvj1WRvzrmmlol9DpJePQwH5VzMPRDIjOu90Vj35pckPhwClkKY6gKS5F_k4o5g8h4BoyciwuqqW3hx-1PQBwYuG6bV0V7dtFyTbSM4sqxy7mZtPM-q0w3LpqDekMga6iqPSuhU5F3lfoFi1iuIjd-PlQO-iKvhw2FLtTZ1bXgvvUKrn0SzI1gmQtPSPz6IzYwFe4d7TvU6SSp3T1N3ovllo5B0nvK9iMqmQiOL1qD3Rxe6aRaRcTsdITyLtMN9Hm9oh9IVX2LHZRxYRTe2xoJTHrrnKNNDvuShHXQ9I0KlgrEeat-eeKIKuKkus52xLG_UE4fG91IBO5_YXbskXb23TSRICWbxn4RWScbUSISWsTq53bHzn2dZ70Gj2nrG05zgrUXGMS1CWceVIC3JEtN6t6NJM6b6E_ppoLejBeuWJMd4732G8MlTpaFy90HtBL_HKuRqACYBLMAU0rgaSCB9ajsaGOA-qAflWfspJalQwNt-Mvz3dEHaHTjqR8kQPDAVLWVlEMOZDw1MLFGfAh5ObdU1plH1KGNfcRi8e8sv1PlFFXQzwg8MJ3K7td2I6YIfrzkb7KN4e-dqwYKRg617K0yM6o_5S0rAsXmBApFJSU5pGBpt9bRIe_r49dh9WUyb21I1IFHe2XJqKNkK14YY50XeZwiTlNLan5Uy-wjWTTzzzfSYsAgsyXb37liWRZToydNjmIVBIH685bJD22xcslAKzlPUQ9IQZpxPAEwLz5oPZO7yaZN6Jp6qaqrDj5XyHDDngEzXqwfvbwf1AtQJRCv-sSHsEWscP5YCC83rjrsWpxX3iOJcPKPPuthqiD30SgCb9mOgYu7ehqqZ1JeiYM8HFATooMTYFzInG05Mjr-9NEzIm6x3ieS32EJChUcZu9DW8axHw6T3EM1BdDTkDe2zrYD_acNblECAVn-5XgNwlNWxOZZPl6U0XbP1KHYcVWJ9obTBkn3HnXOdkj3Apx8qeFbTUHh9Qj9wnc3elM04xfZ0JNtogyelZBIHIA6hLl7ds5O5SJsEHuKIZT2G1i10laoKzSHu5IRExfoFpvYW6VobUEWqngqSJw-Mt9YzoSrQJ3o_QEX6ZH9Ps763nyts2dVK0oDQRNNnT3VrSm3R-OBjH119yeXCvmPwOkupy3s8eLdBoMzfakyTGA55pqpQK_Abfl0xi1Y_UbzwZdnjTet598ODitAtO49exoOvpGI3Tp9AVCk1daqWI2L2j1sdKCXGq4VqCRcWOLeQy8MBSMQjSfvhFiFc5yKK2rpeh-vOaRG0J6Il-kH_JVRLcELzTi31nO40TVfqmAEcGYzCPwzo3JD0Kzbg4SNtgh_Vh7Gu2YptPNAaciDl1kojuanDzvKDi3Malytn8-YKPKkNMItII-relEdfI2bB8Vgdl8up50qT9gyyROWM11W4_LofiDwlx0lof54QN42WI7C51l0-Rl0x8WuDKzQiqRdP5DGUjlk1xe9KpqWKIDmEiLsZMbByb2UshdG_QwGGrXNtw2jdsvXJvpWKvNiq9ChOch5USkoLGH-M1yGAP2AVVrgNxbgHX4oH2sZUinA7NhG3wxBelUKPTs2m7RYys7SArROZ8mPqskkJPuPPtiTEGu4s8dXvvFPMcBaEgtAvSqs2ZyhfaS4JACAObNXZAcVgOWY4SSAhAFESRu6snZLI97M0bu4PudqZU_QJmqO1wfl0lhDauDOCOZn9dYn5QrAr3ywfRxCv1_Au4fI357I2atGUUI9YexYq13fMADH4fOJY_udhKH80bH2IYbwodiC7-iUtlZ8IqRmxi1RjDvtI7GumnlVH1DOEjy7l1h5qvCA_4SMi1cavwjaiTclMsUKX30OZcXh_U4tJsw6TxaU-j9YbQQQIBgP7I0PKq36fw57-HDcDTBLOPVkJZNb0_zF1cOd6KOknFJ0PaU8NQRaL9b9X1BgbSLFnYs6YjFCtMzhRw8qp10Si1txUizzIoPsceM2JLxHVbP_5Eu8QINH3GSzYvGJMoIKrEBTeohzwdeLF9sKn_jlUdA0JZP0CqdMCxzMs-czd3tEJefhcLSc12C50EDbF5owwF5X7AR8hUoUXINlRJCFs5Gw-yzaNBb90_e4-UnIJCDLiLAOvJE8zSkhQRONnNXufdf6UN5M_G3uCKUXDtMOMHOKn0wrogtZ6lhffNzU3kdOdJONAL36jyIPz2WUHCr3QhBggO-ad972_EVqSIow_HI6yajzFJ961-DiWlbZtGfOajIck8fYH3xZPNImv6kASd6MLlbcU8ReyAYreW2N0QcJmloKypJF1qz5TM5-YgnSd90vUrSUZzzRu0rXtS2F4o6oaO0h4oPnnkefXJHDpJHfNt4vNlpFjzxUVDeYt6n0h69YDcSQTEL0WuUvr_XHf_56Kit0sQ91sIAxxb4IOj1i6_4KUPICLJig5gJcKr83sk-EiOMmF7AQ4iIjTPoiXv7SF_ITGkSWZGwGMCBPEn3e51j-f7MOOgLK_jkzNAeRRzPsJW5sXCAsB-Ll5qOeZ4QbuSi1ppC0eryaKzA3s7B7O4GXfLAacNMp0WoX1bQUP61GUJPCMLi9dsUJ0_U7Zly7KXwqeKQcYgLpGeYWN9KvRV00BIbFQOEdfKR_Jqb8n-rVYkhg9Xb4gOcaw4QhN5hueeTHTt8lYhZdMnl6NwDYgs1pAt2lMjMgK1HhXLMhltyXwayVj4WkOQBs-yqegU4qvqOtsvpymPsW-2DwKpzJMyN3xTZFqIYDUSOalMwYpneqnSdyhwqQJXKB8o-zw2yjHKvcvYkmOHh2Cmq3kazTnK4KCtr0_EEJzHPbDVfms9qYh60ipHU_zLGkMSwnDYRpO7Fo-hMKJWvf8bCwGlpmCWPjq618yF7gyWI9wuUNI1lLfN2eFGwLZ6YQwv8jA-qeFEX3ZwEczChrjdtfcMvc_rCLSMRY6fCdtb6tYzaemwsCuVPz5OWf2--uAnmfv71oG6RpmL4ZpvgKsKAV_XsvflHavFPjW0F-Gl0iZzVKaJj9UzBXUvFw8A-cTIttdUzB6N8HKAmWFI9lq1PEVHaOTdv8kcSzh2vOgMnl_M6fopj8AsERb2bF2ywFIwxasvPlC4yWNyVMcp7yDTJpjCowg3yECe_Ud3msEkny8ecViFH8WS1CxoR6BTgNWnE5K-p4xpWrXLH6AnJC1u0PijblfT_hFcIRoiTA4u8MdEoqRg_-i_XJzzJRIrTLiEo1tU6mDtr2KX-w62na9tkOGR12m34ciN9tKetdZxi8wlWUIjjx0oCEKm5jxF5VgM4carLZtZBARPifTVZmJxJ2anz4Yd9k2X_TYg57Z_H_KuA-EPC3lxMK2U41du_EPoe-16oyk1PNpO-tMDLx_InQLiyt_EpSmwZxrOP9rCqYHNNcT32Z_Kkdiz_V0-SD1xCl_V36nm5TX_5sWMQivVI0GBlNTbF2Y-ULxRTolYd_Douvzda3j3r8zJgMTuZgHUwcXz7JmjL9LZ52k1hMN1uvLhuOYLQS8RQukkXKgkfmU21WTq733SFDW9YriwjFP_VZdI_Yp_uQ-KxlVBMO38BxCCqxDcsUZ_BmJr0MU1Cv8nwy4UrZyxW_EthCK-CxKF-HhJ9IoIl_mwRKnnQ8PXznbrjLP1YzcAJIwbw5JsXJgkQXIhLP5wCUuK75cJ98WDBWbvtz7RXSXXM8eK-f00VyQ3s-mxhTeZXeholKRpQwAbbgEjCqw5stxQIz-JcexI-SavH9z53Zc1d4AKg1ti-yGapBlJZ4vmK2Pdi7qmgPS_iRRvsA9teMEcW22kdJ3CHDFp1J4gEVqLj-AZ0AMral7IVGBedYzPr2nlJygyLZkT0MKU9rn2kKsyRO9Jk3Qs8P7dylXdbdz15-_vSx-8NKD9eAlVtxy92jM6zixetRCu4Wb5cOjT-Ryhdt01QH3Uyo6GYHdEasL93vBgEEO97BI3FxtCapgE43-hBbAquTe-yJfGy46T0uU_iicRHeU7C8ptIWEHzOtR5PrI4WAwYpHqGf5dif-E2VK08ay5rsy07XOdqXZCDDOkjIgd2rra52Qlk8Ix_d_Fui-kOrAFwDBACed04ZysoCjx66qR9sjIHFMOJ6iQWmkH-qmQFE9ZVVdKPyt_WV_MFy3Tg5GjvMC1wTxMebcY1P6m-HyxE4YIuylG7re1PWXcqamriMpOrT9Mn8TtgSfzTGgDiFtN2WwyVw2KEJ2VkGP9ik6bc6ISHmPn8zpTbQizfNq14zNuBkzuPExYnKU0csBb87tCX7vvpzVf4-7ELK3saLTeIkWGt7P1VJSuPv_VK4oUJiE2y-eslp8h4ZgoWbrdIH2DjBbyQlqNziTzZdex3ADxMpklFki-lxH5aJrDx9Kk5XkNYp8TbIcHLmJp4Rm4vTCDhG_riWJ9laZIY10DU3RhirzJiXZvqg_xqvqtfVzvHooqSATQeuBtYA-1uAL4j_fBgWj-zWIX2mMB2HnjzGLWbKSt5FLkx8gNz8yj6Dt04gBo2Zy0MPJw9VB4rbj3sobK_haFIim85yhMiK_UmamcSXrqpDGbJH9sqW6utknpzLi94APArWv1SNqXIvNPPNgtUfAy-TWcDhFApwpCysSN6VQsCW38uQFe4U-R-_2rpTRZqSlkLFSQwwJKjo3VMh3VfbIZ6oHbxCG_elBCTaoGObdf5IaOk9SClN-mOoiVHZmN-n_qvwt3Iz9vU1vhdxrM-kvXdkKFrTf_89u7_JZ6YTGknViJUCfGHWZOMZ74r3axzE5ToGJwde5I0iVCGAUszaK0pz6tGp6CUigbjLUf_cFkvGYocHyQcujT0Qqi6kNvIWGn50Gj8CG7pgMMHOI5VEdT5q6-jNT0GB23GjY1yORPbrYkrgWyntjKdEOO3Shcrjax8zt0AyBKMz_k18ShGIlgKmbVgAFJpGd8U6uaN1By0avRypCsbLkToiKPVYXgI9JiGFtB_0GARLGmi7zXpKR3ryx7KJVwJCWq5fKakzq9dPwkV3_e8DavXnU4z8J-yNbVSidAxjbyDuq_diQn1gt-O321MFj6yXWptnwk-k_JXfHSFdmWw4O_wHs1OazuFQxUrK4eV2qvLLvXJWd-eKCvHwWwVVP3Hz4l7AAHxVb9mWTU_Zrxv_5GX3C_7V-Pak08O5r1Znay6JhXLk2ZvEmX0kc9_ybx6hwQxja-JWg_eeo6p6Ti1qIY-9vJKKP-fwx59u7yYYjvLAk15_YLdQ3UAyKzuFXuZSp_a4MYFSP3u90WjCCpDZz0wyU13yJcz-hqz1z2HsHk67-0HFHl0aDMqbzY99f3ZnFyLToVoSpGq5XIUFoxuSXnilFrdcKWc--llUSVaIaxv4EsxmNnjIZo7bl9DupNES7XmLonGZt7BTr1eDZVXpcCxK3ENnaVgPPZJz-HiNFPIjcyMJAuiN6oyhSTWUUS9NIFNqkEY7kJTg237zTK_oNMYe3KkSqnN_ZPokosKgvEjEJ3d5V-ivgrEJgHG9JUKnurKGIPMrqhf6uOPMqwl9J9QQ4_DsuczbB0UX-ol7ICTz1HhsBIVhNbvFswnn6HVBlNTRL7Y4UKSPbKDbyxq3C5XhRx7Erab1SrwrFz6ga0wj0zFh7JdLTCGlS6twTRol8B5-qrSKQurlr_voH96Go0Kydox_U4SD2PVBybA_8dNLUxoE3Sd00vhc5LgcMU_Ish02pmn8JnlRQTe8P-ihpmr7d-4vRq7ABrKpK-8D-TOvspns9eTqolQQJTwrQVgTL8k-wNfSIRTXGKN5Uq3UqrqqobyvCIfX6zG2ndN8EPO3X5HQFy0zyewRVolfDyy0i31TGK5WSkVe8ifQT1m_wY9Mnv5Xq55BCwxcW32HOAl9fOsvaHGvDlSDb4aAdJZA_ETwmhTltwYvkFEXXo_Zdd9yxUTMhUN0nNSXnirPcaGGb9IT6RSCUkeTSY-wk7PGHHFWW0RSojsklgS4Jdtq21C4v7PagbpA-bIX_5Rnpmzsjf-LPRcSBSL_7jpfJ9-DTXJgSvKBl7vyucIQ9FDLE909qgQ5mKcoeHMDwDF6aljdr2uQwJw0uLk8Ec9vuyEXMKOO895OtJmxJlHR9Cio_7uImKTG-wJq5LkNwGx-QrEBqYMff8zE68cKHO88eroZIT0gps8-0F2hSMx3c4l-r45eEiWdj96Or56QDCSYaYWSoTg-oEtj3Te47dEpbuvp_Q8808E8d5c5OXhI33qah4xM5FKIj_fWAeCxe5IcFNa_BGMMFgAdPeKkuCOXt4ixPpQ622gZeIu-F19apUkJDmL8hvrRb7WvWNZLkvWUcJ8wpTrJi_5G8LI7LIo_J8d3LHUIlCs_KukcInWYzTHdoKBBjxTc2QW0OkXb3xF-fGML18Et5RNUPJaXAQtsyqsUt0v_DidFLuiq2NADbJ7PDOzWQkeZQuLJnqhnLeeG7bNqJOO3BzjQFS67mMYJSpF2JWMCzZApWT5cAcwGJQ5wkQiIlS6jiojwUW3AktokNd_N5-Rpebr7HKaHIXaQ-5L5ZCbjPGG-uHaeBhL8A09f4qpQqkwK3EUOdcY5l8g-hc24-xZpxSX84zc74PQBySmeBGMv4nmskDXWzDfdnVUt4zSpj4BCyIzb1GpEkVLqmbI9u47MB8MjaIR6gLK7LzdzYCCeHCTUZEzYCEHIzdMu4s0flzhEBGtjUxua8WUSH5p-_OZW4p7YRe1RI-FFrjD3NBnPlLDnVZJ3OTjuXRWPSfD3xPZEzCBlPL58czrpBWzMllGv8oZUvFJ-oQMecyhY6W3DR7phpR9YPXGD2yKx0jGCAH41e62eT-uEWP0uV27b9LarclCiKV_vNBWOyHJtIbLu31RNbsxWCgLxAFXASXVdmRPN5y_aeH03CBrLSF8OXVI8tkn7KwdtmDcsmDqk-2Hd3PoF4GPoLoaBAZbfCZ6u0wuAwYUTJvEBmpIp14IEVI8ubskELN5Uqj6EG3gF9d0x3sEMGsUw0GunLCqf9paA1JtRY82DE4WRds5NeFE7_EKgr7AJ9yNZoTdIaoSX8fnv3lRIrxIXRvOjfoqTLQBpZJHMRbyiZ7-iq4DcYjUPaO9uGHKidZkcgzcDk35cg9sBME5w_VsacvapS45ppsNSIM53Y7kEX00YxgpQXbjW4jPrd-a_-bQYc1T63HtjWgv2QGJ1GkbzYDXyLCPhH3JPy747heGhDvjw4JXa0HVyVg2Ijt_sLYj-im1iIX-Fw6ntVHhTGRnEe_DXi6e3ZGPT5-mGB6iCwP80xsqlbLhEWPs4aPBPQ0f6N1VHS5AA3LDDsgNlyDe_CaAilzfQg8eES0WpFCDaJwe2obhBalZOJGuhsGnC2QL9mxI2KBeoJFBemffZwtej6ITRDALiyag_bWMz3ac3h-XuK7pndUPCYXtoiSlTtz_5D-ThaUBgnc8inNg__0D_wgV4ddI0l4wI1g7vxgcb8IA7yuCaaePkuxTa5cGrGzTZ9MZjaa3w0MKE936D124Z_5aj63p6djS1QcmrUSCgigWi0Y68eQL4bv5IVjGSl94q92KSgSp68DtzFWIkV-lWeTcDnEZnPOsSa4-NwIRCc6Zddjtn45WcKzbwneZbdmagNW24j9wbGbfEnT8vahu2fos_BFpx6G9C6nfJLwzuvsnNhy-Hb6oPev0fEy-Xve0dZR86Lyjt2kZH0GGfRa8LbzKY3HbpAYO_y41OjK2UlVgeZEz1D0J5qv2mz6JAaHALzcNIbLoaTPp_uPIgARP827w-VYqh1zKu5h0urvnO9XCW9epte6Eg2QxLVdN1Ad8lmEO9ZRhg5j02yMgQQZqm-NWOjECd-veJi9nCkfpL9y6oHVJa7dV4wlpEOh0Z1Vc-7ST1md31SKQq5xSgmnOHaNLygkiLJLpJbMW4wNFDF5k1r6PnyH4B9pLZMsywnFF-hYC0CO2g5urp_yOiGrzopV4Bf-rNeD-57uUz0TujuMDQAaDAKHfXdBBwKw5nb8sFkeNA1hyH63wDwCErv1Gjr7b0s1mNZ0mVMGs9HDW8S07leHuY-j1Xzfd_7fMc4Os9XDeejbMQND966Cy6Jl1_H3N6jNUAgjBOfQ2vwMsC_ZtF87w9tiur62WPfwvkrx6oGa6pnuYfOcRdvvoEIVibkOnspzDVcnwX3dXHKs23OEY0Pl0FtXF2quxzQtLXa74AhowCYykb2iAHtpQbpzbSZuKTbxoHoUJtYv068GcZ9JXETbflNNA_dBnSFLjQ1ldQdwdKfiE__lQlw8pHG8cTsT0hruvgz4BFdvnvNsVcnfHNKgj3LyExum3EVM8JyVC9TJFlo0RhQINRBeHAvC8k9M9koZAlsf7aXNfn7UAmK47YkXbG5gUQSFloNw7LJSAoHjJm8_iFRsv6PDkf_mn7mJ8Us7UyXu2NlyvnKaNuVEmOHHooPVeaaiT1il4XoF8nGbCebCQHzC26EhdcxM5YWzk32uIWOjlaM1n7B0x-5rfYJ5wdji8N6XMP2oQgcdfFW6rlccAwDdy3TvHiVECO1Q8nhjRJKhnHwYcUl_ElVkI2YUzvwYzPggtDeodIJzGrMiXxaLjB-MheON2s67Z5gg4MAFR16juhexdLFg7rSmI5sY4xjaoW5vvAdOR3v2O8HSQoSMphGHZG7lfx-JPaSu1OVvkogKjXcsJl9EILzuoG6NQRMH5ufUWv1Btu39oEZRBwmyC2AR_iXIUbnDmfaLlHE892bBUUFqOKeYVj5FNLISuP_T9CvDtfL2bG-qa_bQ9Fczu5JgCLzpvhbCHCsTh48_axUoHWuQlzt5T1T_xnh4Dt1qEPZFW12LIevo4g69W-_JwMe6r1hd-wT1NmbEHPdrqgzZFLYSgjPi_5iCXNLq0Vl_2rO3BRpw1Pp9fN78Tgxn68K82_J-NPyn58ZKL9WfklybfeWtQBMHw_qkWsEFl7QRwis2Uw8WpAnmato87uGYXf7Io4eLJ2gZQUh5g5eC8W7tZRE-pdgwYcS5yrfBUM-H5M0CI-2e1f73oI-of7V-S4KJiY9NwUV_GSL-18HixmVOf8r9CtuXmQXavfNWTXVFgbf3C7EwKDr2yoU7fhzDwokk0Plc2ddd1wA_EoooPEJvi7hj5FLaq8TP16ypO3Nq3wSU10v-ZOdyb257fAYjYSPx_Kc4IB5HqNfYBeChH2eUg672Vvb-eRpexmNJiHVtkePCQxxi2bhik1E709n3l7lKXEMYGlPKqPKHRxdbG2jIYpEth-aSJzZ2-sJ8WmWOM6NhtDqudc0FAId_sWCF-eXsLb3ZXgPBVST1KeMXhnxpdXV-fXgWsSzlJOo8NmY7OPjx6odbpfKM8WrDT8kZMdwYYPz1Vohz2LIpK774FiZyaq7OW1VIgvNsJ5nzmYJ5hgqkSgOq--4zfM6C0KG5hC9yIDLP1hLkSWk53-8d-QF6RC5lQCtq5cCEVYr1Jz8lAwEAlu0dS-zlp0oI5OX1m-un_Qb7JAJpO_Lu1VFkfR4YSxx4FgpPwHY72oiDjcRNXHEzLPpSYhLijlNBwC1evKm155e0xt8i2w7vBFkVdW3wF2GGUh9A69DYUiJgQr4-ESTne0pDTe51z6kMZXnjUozYfZLL_VzVtDKB6C9wYe1XafJ0qKiuyMV1LVtxQ8gsV8xV0rLP1DfGmAedjYnGyIPdf3IWS7jeXjr5_nq-GH3M-IHuRba0FrO-853eM4HKlCUuMvCmHenyZSrxTmfkA00DATA4pSoTQNqX0a1xDOZkMReymTI5Fu-G4H-Bi6KwLfjJbh0SJBJ3cTUiufdHPnrj21hNZtTlankniO3wAgc6iM4jDS_9-ITCLcCvnomb8_7NdJt01KR0EgOhpjNsnXuKXCjRkN1U8erJ5Ak5kt2yWzQvVt2je2yhD-_s2YeFvDPNbFBoz7c-3z719-gbgR_q1mIcvGCVCFB-xPRPfvQmT3Sj7V11qc5b4OW31JC8qcIiBD53UsUdx2i6RxWTnD_55F1CzSb2BpjiJYTTRTaU1BBF4ceVbajhAQHfH1jP2kxo0r3JBNIPgC-fKHdNX02I--0a_KAZKJTdgBTVSbeiSp4GV9b-zxYMJw2_5XDzSAYMYLqOR9V2L7mvGuJOReN2rr8Ox8X4kSin66ZN7kJMP6KoG0-fxWIiRazWvlfiMOFun6jS1OTovi1S0UM1xcCSH7fkICaFyg4PMHPr5ELrfgPhSpEMqCY6EJ9fhtnGO09xht3MOm6OgfKTy4Q7O3OTOx9R2SIPB5WPKBKwqynPZsUE9o0p6F0_RVnvEpRRvZ8UUIaXvcP1L4xE8DB0CXgeAS0PuF9O7g-9ZSJRBXgG3lXiwm5i_YvpYofFpwi1rwZ-IaZ2YGnLj5P3XgFJyMjbt0OwkMeHu-3cWU2BxND2Xsao4L_GBnSC6ijot8EIUl78xo-sTnGvU7M3wTI67b7jgFQXdU0aEamvpdGp_ejT1dKR7cGl-Pnj7Pl5E_8E6vJ5ltDopruprc5_k4CE028SeYd5cfCuWgxoDtzk7Ras5KAMZUkSxl_1vlc9ptENkkksMRAQISxlgYyj5-toa_-l_9E91MXBOGQ-REoJRrKpL-Hom-x0nfdWdtyuCCW-TG9OfoCPQUQnFtQl4oxz_xa5eshQ98bqnkzQLCQ5QuGG7oo_piRM2QIkuOeZSt20xCbtCkJkWagqkx-VTuyHqdj3vwOxGriGq0yG1t-qwlo5aomX0pCFhapyYm25sipjssSaC6SVUEF8EuImpnWLmZwdZKMeZAKZ-4r5_B0Us20E2wTUzcWZHD7NfZqsM7GQSN1SLh2bezV39QBlXZWf7WVYx0So2mJ_aVySy6S8rowBqZyzEagSkuTVOYqxkjHhHoWI6rHFi3E5ZwR5VjmI31Cl4VqJ1k8RwmijXlokVToOPYatNdy-MmUjtxb0qcoAKmofqThVVXnmV1SakRFDszBPLld9L4OPhrvAPmY3_83Ny_2v82Sl5qF0L2o4U7xTm44ZjC2cfrfryS4_wz_JqtQe7JokoE2X8pnHDqFdODPlfVbLE_iQL9K34c6xj_0h-k98KHaEKn_5Q2AjEbpnRzJQ-gw2vHWovWmHIe4xwv5jbZAo8-rt69M759oLpPbumpxvrgO6lFzNuZkDwtje_XDn3aIfZ4gvRy8EQf1digxw-sBZU9KD3oEs0cdzyOnIFRBNAQxKB2wfHbToO_WoCcqFB8QpwH5dTkEi9neX1n_D7noILXYj7wui2HlUPYxDujav_0TFS-xQ0zMPCviBKOh09pkFA912r0-6o2B_ZD4juzUHtBF1pU2_iXxxPsqdzsGK8zenapqvWfeiONZx5o-wK_PRORxWsQlfrjKSZ9JkNtQakhpwmzXJW5hmxD7_nCbdoDu1TgUt8Q3mL8KOTwvAURhDxBHnxnXRJ7M5HnE5SOoQTt-b8iOwexrAmInEoE4OW3X1VUSEfzMQi8-yhUQbKpF4NiJkche8OY033Ysdv1E8TvIRRmKCHSZpAb6yJWLQSvCcj-pol9n7CbpvK3LTZTX8-FwOd1Lp-P_d6Crd8XSx20LSi93wCNCuVGd-EvKczNNtlafxXJPjkpfuRMUkVQw2MtbGuupkETkCRDXI9kgGQvDenKhbe9kf42_dxgHBadY24q8VjA0T30stEMJgWtnYEaSNNYvfypdF7p8pTP4iSEe-kZbphndXjSygSRFC7TcdHJ8z5skHhNvgNqQgcsIHD97aKC8121lUcdNRjOicrw5EYsDtJmlNXyEiz9qlZu0DTo-1aDpVMOqc6S3RPL_ZmmFgb8porykLNDoJLLK--K-WmvRSLCN8oXEeaeMfUsRMdkI2CnuHgqWlCKW0Hc1B3x5cErHZgJuhTGlBapexzc8WON1ESerLwh-YYlyhfXvKKTslxFxXBRdVM-T4Lf7Rh5iMxfjR_kuaciWd-zdkZ_w0fldUnAa-CFC4qF-1iA50T5QyhcFQMG3OqjpxXOErhTbU1YFSd5YKh3QQV2let7iQgnJhyKRNnEqvkckUttkQv547j2tqEvQ-69Rnm_fOK7zU4cT0jAJAHX0FlqaDv2hu3iDU_WPIrPICvSW5UMYdPaipc2oTkbz_gukpTE1Qiroyqriv355rCyAnNQn__OpWz7X_zVAA2iFXdqzRVcOTLNh0HfccoN1bcdJ0WrYgUTza2srvdW72mbY5mNJZ7j8M4ah-lL4TZaT_PrqtYhFtAXPo43BUNuu6-I5-ciRtUURmVI20uMswLXXz8egnVuYFUn4k8zrvwlyLbjleg6SS2spoPeyapufM9DO1n-UoiJPU_emu6GjK1FINr0OTbhdOw-OnElrgw_gUe5Ox3PKi-dfEvOYwqya-ep0TUhdBSNzocgLn02BOthlmqoI1gTkEcnHx3-lwraH9eNk7EymCACm69g21I44QnEOzfxp9ldjJsq35oos4HPkeWB9XDy91FMGXsTveDONrJL5ZlwPXdhlQ0z0Cr4IS4XmNE6GTWqn-8agxxZJymFatggKM8DOfzEcBaKBa7xUwMIJNaLXgi3ebELI4PjBK7U3vnoLbHGgPmmjh6dTw1oiuw7ZPoJjEySXhxegsMhB_5k_yRF6NO0iy2tLbnYaclmfxwdUrXnFqeecI4VrE-ljgtN1097VWJe1M-DctoTJZp-gJQ2E71pvduwOc3iVDI0rI3HmIV12Bh-ycBecVxnBN2s6Ly7z6kxmUaB13Fnt6k22DwHBrqwwtM9fhPo8piAtqB2LOK93V8hkmj3suB7yHbHVYQAAvEQObGvYyJkUTO7Xi1QwPZfq0D67SklvOfdLbm-gCpmkr53iClaXn1aziBj9mIVcZYZ5Ip8qhrW46-zaJJ7HIlltmil7IPKws-oo68SmXe3Ter3rdq9Nhiuj-Xpqb3OzKDuR_BHqSzqRpb02AyYcUkE1lCThkJOQuHDvBCRYN341GEb98Y1dxk-A4Im0VWFn_RPiip_0aE2ox4-3MzdJz-tt9Pm3AtpcdFQULpPP1zzi35f_gRnAHv_3NdbEtDpr3mXZPu6vZiU1PLG1KkCsSFooztnPaohtBOpJdclDf_U_82sXFObwaaM7pdj1JL2EBh7zKu3ck_UMby0Vmwod9_MkGjqkdt6vdkeZnSOFuHj4jeuJ43UCPFJYrvZJd_cSdiKT-jjP_8POjuNEi1bldfkxTdkB4ddEQeWatfzxris82IpYKsknTehc34fqbY1Kt1QdEJTalaDF2hqwKKmW_FpqcndhWLgNOOVXd2YPqm8_4LDpV8TIFooAAzueI2Ilkn4cKuyra4GrCcRNjfNVqFFwrkwfMdh12yRq9E5KRf0ebfX8SemaKkBCrFxrHfdeP9n51-7yi-d5F95tQs9RFpaSQ9oBn710PpcBTpEDtzMFgUfHWRGpa4mLvBErXDsBEUnThkV3qmyUrNUfrVFCCc3kQygBH_4a1T1E_wdBfoUE1IDId_2mnT_UOGaGFZWwEYvbEwXmTusBcpm1txCeIhA11SJKUQRDFCwk6eR0GMBwO_JiSwBestTkHSjUFyjeiDZB9LvCbMIyYkdf2en0tGcisZR0WyYqjtHvLown6jWvsHGoVGJ3H7i_keNGLDs2zte1_CvIcuMzdy0GcUsV8VmmdG3CauFWZd7oV_SwmzEI8-xlfy6d4YjRgk7CGQkXmp5tYrz2Pn1IEHRU_eEMJf75iuW00pco4xTqZnh6XvWZmVjIgus6riFDv5tpi1fmLjs5gZhf0Q0_j-61JFgQKvZp_5r8T0nCvc7oGRR3xdp5a97eASG2Z7I5paL_-fNpg8r1v1CxP8Y5e0IrcVw9cZpgQXrS5m2Pru1-4WtUg2z7Mcp7Cl5nAOk3rYl-ZO2WAMPHkXiDaDdsxaJTUley6Q97l5jGJV13G2RfMKeAmJ8RBYcXzfPMpx_jlXyfZulMPweRTSAJRDlU8I90ToEcrdxAHYlw9-_dcrMR9eAdUfwzUDVWFAI05kf13Jcq4nDim_uHZj5aLtFIKKkOR2dcKWxRo1za946Go7DMxz39un5rZOC-zM9wUd8cWqSnSc09Jtni-zN6YXLMOifjqOLxZso9yLKAAa1JyWTqi2Gf4Ba9QpOw6NlP11pxUDTcO0dXFr88Mq_QqmxCVhm-mYTq8YgDTIptGoKqwsIBlsl-vWOnX_A6cMTcRCXJOVA07aj2Uph2iLVqODKtHH80r7mqkwgTX0kz07SCM-l7ILr0NBjnofbZ9jfuEsevyFSZHbUet2aZ1VN9sG886bWXvIyb2GKdms2t-9UCiceBw1WiWjWz-_sTy4NQrw7KQxrrUFXfuDaR5FX-tYWlTerVxcsRA9BlBsWOWPgap2pyXnT927Q4-ZvwEe74-eumwXOjJMxbFeBaMme7Vk3gXLvJ4xZxF1hvIViYBx3tLqG0meaUiA18cJzAV9G3xbKnHAG67i9fRVOUzkVA1Z-pZOCzGgn6Psc29UIBWah7RnjGavVZGf6f46qNnl2gWyvb52CaqGxwP4cMQPUw2NObqIW0xOALsVvwrWBRkB1MtKKclIVHOr5Xiyr7bEvihhfzSprGBCh05I9IBHDn1sa1pUsGhO7WOlPQcSPCWyc4Ie2rht-xiEpYwjtjmKkdSGBqbyOKddDWSMgxHcdrbFJsQ_m5dWPzcBnKq4UiT8S0jDSwBqGxSaYX-PXtR3gCduNioiHSzGFO_qCui76g963qcIToWz3xxD4xCrLp2ZqQ7BDc3r8Z_OuLDsINR2nHOn2GSTbX2J-igOhaC4neq49N1TdwOwNoB5GRrHddJQqmqpn9GLlIdYHQowzXjvpO266a1recXEKP18IjblpoETRw79XIeyc4i9-dX6oEcjb5svLUzLHt4X8gTDaxRAUrEO7TOBLLwVoqnSDAigqvPFMO7KIgm72G_O0CRwUOGghT5iRjG0AUdI6sE1sHuP3R2iuPX_df6mIsjz5maTdixCDWFx5doyAGBhnEvujt0lFqWr1CSpu6IIajh_U4txMc2cltPWjKb_KadxMdfcVzy_pW7H7bV-TpTRengrLxAS8lAZdfqLwbknl9qCcnI56vttlPQWk51lupNGA9gFUYl52PYksevrghPbnoE9Xr2NbXoHFCE0doVBiarLdfILNoYBxmLLZqP3xVLpNBoLWnv6NcRCpZGMAqHcryhcImRGNigkEBUfK2kRg9PIRMURJH-sQdIItv3ZmAWA_CRWEiGpCWF-p_Gep1MKD49Ot8QEiE4k_3t1LhqvtvQ3SQdprj23SidfpR8-D1QJLVyUrjmceoSCZusZ0LSIsmdN6wV7ahZTCoMPm8zXEJhukK2Fu-0oEbS0YLj-gCxr42k9ay6GsSHHBOt1X7zSob9c4msukA6dlMi5nHaF9RyBa50EGjw8ZvkBbyxDh9wfKGjnLgifNQd1sc4ApblAxlSPX1XO7nckCUbXF3CrSHDnBT_upMGc1RiYHm9K0Ceq9qGhEeMTShSXr7kWim7YNRtNUCUNmwVWU9dQ4uzRVcxr4EqjoRlJfXMcWE4TQWevZJWsRdpYgLnUST86eVcayvyjjgg1OGk0s4lvU1lsO-JMKhYPz9a5tYHaEf6y_2PejymmSSFBGCnvDPUhzyNerAPl4zMYdLCUoUlG5L3Z6patU0njH19rkNHI7Mi8jVtxsOeTE7wdruXEtEm0Eh_dsTtYMcPaGHUUXJW2UMcV84oWpZKvLCg-wUEgJqkNa_PPRgASyLFiM8EUxhRkqxyeyKGk42MJ9Vn763vbj_rRCdmlTsT2PwYqr544390yRNYbakCkyY6iS0jsuIdhS7UaxdGtWnd-cqLiGxLFNLrOTQ0Ud0mRI9HCNFs0pa9xqlsDTbnruhsYZC5yKycDZHr2p0rCFoiixZDnIsL2csecibswFchJkCQvT33h4ex4YeuQzLmFKAynlNyRQSO28bM9C_KMP46FDPz9bFWIKcFmSHTTOZHAftMUt9KZTVgMErXSsEks0-zwGeK9gME8QwQIcksv0iRaGR-MAVEMwY6RdLkz1Z017zyNWc-Z8UbyleM2eTLPeATFfY2njOwxHC7Fa1KYXHMqtAjYjzRLAmwF5gp4rd2XNkOYV9t9CSZgCFuzxYVZas41TehMRqluO1Nxm1XG1lHwh_oW4o5yuY2JO5PhGEyufJJTnbxiv9f5AYcIvMHuZLOTlk4odSwoUFeEvZAGGto2M-Qq0uIKvVoNnXHDkTjZPOlUfVr4uhVJJaIdbuY8oTRJhKfFiAlaKNjJruO47B5D4pAQBb_awjPFEhkWxzaSynRL6BEp3CGCDEiBpnp5XKwUb8YuEWDRKlir-AyUKCGJepISftkInXWXyJLwqTkuzTDzhZhy3S6W-HP-bEjpc4LFKsh6bQZBAK0Wlsz60vVR8fWg0ow0Z_MvUiFSIOeHTV67qQm9Q9BP2-r-TDVOcMoN_rPOkpEPeqD_6tRzGj6Ytz-IRmVY8USoZtM_92isv00Ms-4xh_DrwldkwQ2YaHmhYzt1WfG_U6P-07GR7GFoW0uSm3ClLq7gpdo853t-YQRRMKFOiFCzKUFfkqsK-qtI7OmsL7R-j1FFN0UGCqi28fjmpNgcIp7WvtaseJUMydVQc3a0wl53XpBbtH7Laspv-IEdc7MuS8Eq3_5dtIpE4sD20fGK4LRhm_DG6hY1mX4qxkeXD2ZWVdjxZ-NRbJzEIabTFMxyzcSdt5vCWNHat5JIlce9mWnDBaSH0YX430vq0Z8-Ht4sZVaGvmnoeeYAD560WlC0ACjwYY2jvhWR8F_iUfYBPqZFsEXWsDa3HV9ylqhkeGNM60qO_-0ve9YJSyDecI82X-5R2pD3r0liJk-u7WGS8eOCxqWuLedmi-3bDB_RVxq4vToerlfINjTz00gdMSbb7mPi4FyGaUBoDocCK49_Nnk3wUK-U86UFS-a81XzN5YiV2-9AkJjop_npvC8PBJAL_SQuLQvkEsC9SVv_d6hwGCWzA9Q_m5SbRAXA2hh_VZOzIAhjk1nHgCZ8Egx_KzmQ-AG8JjDrHv45ikX5GyVeP_LLoZsqJk1b86osgMG3SLomHSdqJDf16JKYkRqzZvilr6Ac5bu92PHsbQKyoDcJ1KQ-x_dHEKav1kt-NCYPUHhOWV1ohvINMnKzvW-z52Bk7PeJI_7dsGemS9Gi1Bt9G6EX4m26ZxY88_XhWP2I_aN8xpeeTbo0jZzGXsE4ykWiyEyG8U5ZW8pWA_npVrKwC7KYwW1-aJz_PAXCJmiMPgMXDVjZxwBKSL12LrX-GnxoJZstLDYcwGgF1KDc5305XhXA2GtVns5Rq8JMjR-ppOE1WhdYFVj56hNqWAlrbg4RMWsBWrg1cTXchFaJlFYn2LrSNhnH6GTpPO3wFwk8z62BW7A20PWZY3ZF_cyUzv3Zu0WFAGYyeo6X14R414q5azY005jug5k6b8X29z1ScatMeSchJIpDDdyrkHnucyTmlKVBzAN67P-Z8wla3o6n9JaYnIuhNW6iCE1dYdw8-FGXj5L42b7J3VhmgqRi4l5Husnzu-Z36hDIWkEWrkHrjTstpufeBnEhgdO7yzrmTBfvjxua4ofG4WbkvcYTR9YyGP0BTdhBAyCr8l72hVUo8aDvuBnDZQkF7_xpB_hb3qS7Jw_fEejDkifzv8ZDzfukZaIAvXdv2GEQdooY6_c0J-VDtvo9kbEXdch_NKix9k8VmywNJ7wG8zr4BBZAz4RQNZD1f1xzlgFt4CWr4aNHWyfMaRDikccANlk1L3rJRfmBQTP85C4EPVsX8qYRlt6_ZzaWq-0oVFvOvEOm3kf2RPRw19B6y5E80X7eHXZSLiB62NI4JcEBP7dm-gQxWu3sTqv9jhYF2_-jv71pPZoH6XJf6cyFFGQJeXqxwcq0l8Ekbu1aXQ_iHJVQ_HhSq-K_zqvNmTXAE-tDdFxX2uw3Wf-oMjuAPAW_Qy1XJ7iDK_xJksFJbcsve85o0QwaNrlgBIr2n24IqJe6Vvk19ai1mfbrn7uFPkpzqk0JK7MinMJ7h1NBPPoNVb1A_w3kVsSujs5qq3Ce1ntBp7aTt_TPERWCwMX1d93feRQGUQ_h5XaRarEVEaVis-QV6Gt4Z8GCHq-dn9V5181FD8kjUBYgUe_-CA_fnUaF69FY7NN7yd_gWNOFQmvIxmx9qH4WYr0kOncyl7GUiemJrkIwYjXscRGS3QFs1ScrhdH5TqqFxZ85W1jEqOyGv-FsGJ2IE7LpevpYpxkprZi5_U3ws0NTsFr9p11AHfUemBw0gpTKpB0ZUhUeyI7wtQVqGN8NUvKo2IE_97dTXqRW7olp8wufuFyfVjHRTDBZ0UwaQK6i3lPFMfHcs-LROX7vH4rLqXZW9H0YeRZSU6iMGESLbvQ7-dBI-9F1BQ7w9pMKsBT3KYwMsr4_jzWRIOZN7Wu1035p4qYH9P1Rc_d4pMJxBaJ1mHwxnNp-qcG6CbH0x4RO6nrcCiqMoRZfIfVE0j6huuP_z5EngmnMqnni3OIo90FHxpsifpREeGNG-rpBuoIraQ7xdFITCLiWWN3udXiX3Xi1Atjw9r50ZLIBS2v63_vmKVp4sh3suo7GTY4kLo4rcWsoO6iqkpXdEXlp3TKDruVUj9Lj99ozqfeqL53mUNZTQd1ofhVkq6925fCXmiImRoYWLn1A7TAo-ep-UAsjFpPUSG8U3cY5yVpA5OlBnlKkmHFKpkJAeIDx7mq3fw0OXZJygzTyMAflThDtsUSKH7KoW4TLQmjFRt_9H8NKAR3g-p9XW0VWsxjpiuz0Y9FE2liS94-Vfcn6aj26X_pOVi7a-oGHZS5QTtM2XP6L3HTdAg7jwevurAxFeL5yXmvEDnl8_pTPisznc-5Ghi5IByptf4id4-7n0N3wu6RSC07Fae1u-5TBJtcmUpZme7QPzee3MR26WuSlv1G2utYgofTEpeORb0Y2XE5FwPohzIv8jAFrEPajyEk8Z9sVVtET0YDNq7mecf9F5oDIV5qsWBWA7KcfmPdztj3hHQ_X1JFrpRbX8XXnN8_Dq6ctpTJf53KOevENTDYtTOVOKFRrjkAHc1a06eqlauoh4k27EFnLM1wkPKxRbr8orKD-sF4REwZesI4wo_wi6ytNxfjCl883AZ_R0tON_DfrUeisgyCImC0GvULpnzopE_Gea0O4EhWNdzQCzywhmSeCVVWC3KocTbJ40M17t8oZ9gTImPxA-LV5aYq1laRS-onN8Vbk3424IRp8eE3eMIFFduZ0cAESmsHPxi4VP4lzbuhVn06ROjexHoy_wEm19-sRbtnUgNr0CltqTx82TLcLTOTAcyfgQePssYuCukFEIHrA9zpcP4FWwKJxbu5_EA8gd3sxHWOp3giIs1QKJDDotgnQZB0wg9a2hfPUJJH2bH9uSVBeUfg2A450Tbzrl9Spt38tLA1u8GjA6bHf46woC_2KRF-KdZUDUEjxx8lqha0IWPNZaXVx8ZvrPK7Td0b8qKRjybUiGxE70ri4Klm9Iw== \ No newline at end of file diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..1457ece --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +.git +.gitignore +Dockerfile +.dockerignore +npm-debug.log* diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/.gitkeep b/frontend/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/.oxlintrc.json b/frontend/.oxlintrc.json new file mode 100644 index 0000000..6fa991d --- /dev/null +++ b/frontend/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..df9fd78 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,25 @@ +# ---- Build stage ---- +FROM node:20-alpine AS build + +WORKDIR /app + +# Install dependencies first for better layer caching. +COPY package*.json ./ +RUN npm ci + +# Build the production bundle. +COPY . . +RUN npm run build + +# ---- Serve stage ---- +FROM nginx:alpine AS serve + +# SPA + API-proxy nginx config. +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Static assets from the build stage. +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..d6af7e3 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,32 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the Oxlint configuration + +If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`: + +```json +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "options": { + "typeAware": true + }, + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} +``` + +See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories. diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..de656c6 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Migration Evaluation Platform + + +
+ + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..f96a5c0 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,23 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # SPA fallback — let React Router handle client-side routes. + location / { + try_files $uri $uri/ /index.html; + } + + # Proxy API requests to the FastAPI backend service. + # `backend` is expected to resolve via the container network (e.g. compose). + location /api/ { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..d424be1 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2713 @@ +{ + "name": "mep-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mep-frontend", + "version": "0.0.0", + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@fontsource/roboto": "^5.2.10", + "@mui/icons-material": "^9.2.0", + "@mui/material": "^9.2.0", + "axios": "^1.18.1", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-router-dom": "^7.18.1" + }, + "devDependencies": { + "@types/node": "^24.13.2", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "oxlint": "^1.71.0", + "typescript": "~6.0.2", + "vite": "^8.1.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@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-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@fontsource/roboto": { + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.10.tgz", + "integrity": "sha512-8HlA5FtSfz//oFSr2eL7GFXAiE7eIkcGOtx7tjsLKq+as702x9+GU7K95iDeWFapHC4M2hv9RrpXKRTGGBI8Zg==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.2.0.tgz", + "integrity": "sha512-+XMav+ZaXkZKUFUgzjrfMEedfyJKxxviAske2q8N8CWDMeqZdDU2lWMkiUPiB388hGaDqhwvOAwkrsc/pUyp8g==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-9.2.0.tgz", + "integrity": "sha512-VgBd3z7Qc3vd/thcNSMC03nHRh/U4DzMUd+1dRyJTbm/hGo7+N6N4GDuJZDNHa6LZhhwG6Cu1X3DNvrVv8sNag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^9.2.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-9.2.0.tgz", + "integrity": "sha512-+YTRSgGKGrrRo2XJZXs7JRA6qHoHWvNtxyqxnrRJTBmIuLOUpxxh7m4G9lF4tWberxGFY+EqkkRPgJCl+fSMJg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@mui/core-downloads-tracker": "^9.2.0", + "@mui/system": "^9.2.0", + "@mui/types": "^9.1.1", + "@mui/utils": "^9.2.0", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1", + "react-is": "^19.2.6", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^9.2.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.2.0.tgz", + "integrity": "sha512-w9wpyDxGPGnAACPB2hKhCDmILJIAvQxrfjUbIAEa0AznX1rOjaz5N+yB1uuw8ixnJcpEh/tPbD9oEe19wcWPHw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@mui/utils": "^9.2.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.1.1.tgz", + "integrity": "sha512-neaYKdJfvEG54q8efHLJR7swpHG/gfSv9xGqW5iTSMsubD7yPCPFrhVBt284j1DOF3uZaaDJSHQL7gz6jGF21Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-9.2.0.tgz", + "integrity": "sha512-YvUJwKoGVtbnOm2PyPi5TvX2d1rOA6sqSpEWVs4WmXNIaFTuYmNUaVdU2o1NKUEe31URnD3E8ZVUMcsLQXwcYg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@mui/private-theming": "^9.2.0", + "@mui/styled-engine": "^9.1.1", + "@mui/types": "^9.1.1", + "@mui/utils": "^9.2.0", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-9.1.1.tgz", + "integrity": "sha512-Zjt7u8wNvDg40rPTGoL+TnfkpuSKjwubsNSFRH1KAVZLcaV4I3AFNHIFbvH7p4F3alEibSbdd90xAgn5Rnfndg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.2.0.tgz", + "integrity": "sha512-OsUH5zhlSOM4xmLl53+agug1M1UyWb4zxFxWQCqwKTKUeQPvTENtg3JhrroBD2qpCLKsX5W/DYGERJ4mBUbc8g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@mui/types": "^9.1.1", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.6" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.74.0.tgz", + "integrity": "sha512-+gHd12muVI9ZLBaWLPkHt3Fj7jihFjgQ1MGtBaRL8vWrWrI0P7dLUty/cHrHS0oqPYIRgQUJsPu2CExQuMcwNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.74.0.tgz", + "integrity": "sha512-xjKdoMB+H+RCOByv/7l7nfIGW9mlOisqYdcyC75UqYuQecLpReAeEYUf2CNeDEI3KtmUgxpRw/+c63y4AeF/Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.74.0.tgz", + "integrity": "sha512-iUK7wvc6sejMKsC+Pt67mntoF5weFcyEunhZfLJceU6gL419mexz5wBkSx/EnkFBExMLNtOi9fnDSc5xfK0IzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.74.0.tgz", + "integrity": "sha512-ggKc/tn5SJ1u2yG2izC6VKODfYKV8MQ2AicJlNzOjuyrC29udvOef6/JzK2r32xqCnBDLFouR1VCkjzEI0/N9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.74.0.tgz", + "integrity": "sha512-u++dH/43jy9hTLbneaWlS0gla/Bp1JdwJ2zgevCl8nDFUh6qRCGMxcL0f0lb7By3A9p/LfFr+7cG4HU1hG856g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.74.0.tgz", + "integrity": "sha512-Sj1zmtFDVTPeIbIz4ZfcXAbFHqCmKCXdCUlAJzvTF7I20NTH1RDpoF2PhkqNODutJzVhJYmm3oz0GwgY+tvE2g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.74.0.tgz", + "integrity": "sha512-//PKyQb/tQXcHArx2f7z+oVI/eMS2Jpv+edNuAtOrgIhWdGcpHxogveAxzmF2rpH1AIHp4Hq04RF/rgJdiICnQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.74.0.tgz", + "integrity": "sha512-/k1Me+aX2tjuH10K62mLS0y8cLkJBHX6Ce0xPK+eWeel4bSdEGZ8dv4+hYMzg0GrSmjwy4yAYsDPeEeKBft/2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.74.0.tgz", + "integrity": "sha512-3tFSjBxc5D8/zvjEuLvOqcA8ZXKD0+6NuaVO/edeamNc49MoAsbfaC9s1UiwODwgF6slGaF8yJA2TPkukd77tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.74.0.tgz", + "integrity": "sha512-9QggtPkSPXOCTu8Szis7auOK/sC7KdQaN+/TujP7YVVhzCAOhgdRfgv8uEz0r2tk5xdgus5rLYUrCDoZNtiRUw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.74.0.tgz", + "integrity": "sha512-VM5VPUJ4DJIWiK+AZn8FScUqMr6OFrCAYybMYjEEi7W13ParI64MByiXTkKMqZpBmvQ9zxl9Ebq2VUOiZRJYUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.74.0.tgz", + "integrity": "sha512-SaDY1gh9rOA592J54g+gu5hkOFFQBZsMmIYHs+NRHG+Uq0OxtuuCXMWQ3vu1830Eugv5uMXyjG+bv2Z9y4IXjw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.74.0.tgz", + "integrity": "sha512-ZATQeHZCyr6MbDveg0obD5sxLHFOghtOdC5jwVwYlvFWqtFOxctgFEG6Ef/64hYvZrWyhyCckB10AelqLopeDA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.74.0.tgz", + "integrity": "sha512-+aIvJyrdeD7LwCQ2WYLMUWNmnbeDRSPb40aBYtPjD9+PTqUwgJnk+HK5yLfSMeqXrMrDhE9uTmtt2y50tvjhHw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.74.0.tgz", + "integrity": "sha512-XyktaR8lhK2qWiCK0Tk8oYD+/cgn+oHA6ddRnxSSXUKkkojkV78CmShZUxQF+yrBFs0SuW+JBOPG6hecyc/iZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.74.0.tgz", + "integrity": "sha512-mzbjrPl4neaVUiJ1fUiEUxTGaSZBoiKtaoB6jmIpz9S+VOA2vDYmJpihQ82w6178V5jxziclTg8Cgj5yF6tTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.74.0.tgz", + "integrity": "sha512-vUAe9okpS2Oa5+lX67lqHMuNUvfkleRKwrUDJ/WJBsgmddvZ1mrsh2HVmuFDRzqFELhaJhFaCNOuR6a7L3rtIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.74.0.tgz", + "integrity": "sha512-yyXXJyYYSXL4I8K8jAWjJs+J3fa9gH2JmEbo4f5adm+1tNC9itseicBNuwK7BDHvqQ5J534s+yDULu89vYL2ZQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.74.0.tgz", + "integrity": "sha512-VTC9IYTIMrVUk/i6Ms1ohzzDKZFkWn0KU2OBbPBzgmVZ2V30165T/zK4LztTr0Xgp9fZ1qQZ1rsZAu/rEmySlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "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.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", + "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "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", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "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==", + "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/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "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/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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oxlint": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.74.0.tgz", + "integrity": "sha512-odGl2s2x5IOJoj3A0v1k0PGBXVFBZeZ2+AK/+K2MJur7Ghi3bkyX5NuLUWHKqa4js1wjep3hJeuTQJOlr+4+dA==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.74.0", + "@oxlint/binding-android-arm64": "1.74.0", + "@oxlint/binding-darwin-arm64": "1.74.0", + "@oxlint/binding-darwin-x64": "1.74.0", + "@oxlint/binding-freebsd-x64": "1.74.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.74.0", + "@oxlint/binding-linux-arm-musleabihf": "1.74.0", + "@oxlint/binding-linux-arm64-gnu": "1.74.0", + "@oxlint/binding-linux-arm64-musl": "1.74.0", + "@oxlint/binding-linux-ppc64-gnu": "1.74.0", + "@oxlint/binding-linux-riscv64-gnu": "1.74.0", + "@oxlint/binding-linux-riscv64-musl": "1.74.0", + "@oxlint/binding-linux-s390x-gnu": "1.74.0", + "@oxlint/binding-linux-x64-gnu": "1.74.0", + "@oxlint/binding-linux-x64-musl": "1.74.0", + "@oxlint/binding-openharmony-arm64": "1.74.0", + "@oxlint/binding-win32-arm64-msvc": "1.74.0", + "@oxlint/binding-win32-ia32-msvc": "1.74.0", + "@oxlint/binding-win32-x64-msvc": "1.74.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=0.24.0", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-is": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", + "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", + "license": "MIT" + }, + "node_modules/react-router": { + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", + "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz", + "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "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==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz", + "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.16", + "rolldown": "~1.1.4", + "tinyglobby": "^0.2.17" + }, + "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", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.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 + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "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/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..1d6ad56 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,32 @@ +{ + "name": "mep-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@fontsource/roboto": "^5.2.10", + "@mui/icons-material": "^9.2.0", + "@mui/material": "^9.2.0", + "axios": "^1.18.1", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-router-dom": "^7.18.1" + }, + "devDependencies": { + "@types/node": "^24.13.2", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "oxlint": "^1.71.0", + "typescript": "~6.0.2", + "vite": "^8.1.1" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..2023e19 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,28 @@ +import { Navigate, Route, Routes } from 'react-router-dom'; + +import AppLayout from './components/Layout/AppLayout'; +import Dashboard from './pages/Dashboard'; +import MigrationRuns from './pages/MigrationRuns'; +import NewMigration from './pages/NewMigration'; +import Validation from './pages/Validation'; +import Reports from './pages/Reports'; +import History from './pages/History'; +import Administration from './pages/Administration'; + +/** Top-level routing: all pages render inside the shared AppLayout shell. */ +export default function App() { + return ( + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); +} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..004f19e --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,33 @@ +import axios from 'axios'; + +/** + * Shared axios instance for the MEP backend (FastAPI). + * + * In dev, Vite proxies `/api` → http://localhost:8000 (see vite.config.ts), + * and in production nginx routes `/api` to the backend. We therefore use a + * relative `/api` baseURL so the same build works in both environments. + * + * `VITE_API_BASE_URL` can override this if a fully-qualified backend URL is + * required (e.g. a separately hosted API). + */ +const baseURL = import.meta.env.VITE_API_BASE_URL ?? '/api'; + +const apiClient = axios.create({ + baseURL, + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + }, +}); + +/** Ping the backend health endpoint. Returns true when reachable & healthy. */ +export async function checkHealth(): Promise { + try { + const { data, status } = await apiClient.get('/health'); + return status === 200 && data?.status === 'healthy'; + } catch { + return false; + } +} + +export default apiClient; diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend/src/components/Layout/AppLayout.tsx b/frontend/src/components/Layout/AppLayout.tsx new file mode 100644 index 0000000..ecb2b3c --- /dev/null +++ b/frontend/src/components/Layout/AppLayout.tsx @@ -0,0 +1,49 @@ +import { useEffect, useState } from 'react'; +import { Box, Toolbar } from '@mui/material'; +import { Outlet } from 'react-router-dom'; + +import Sidebar from './Sidebar'; +import TopBar, { type ApiStatus } from './TopBar'; +import { checkHealth } from '../../api/client'; + +/** + * Application shell: fixed top bar + collapsible dark sidebar + light content + * area rendered via the router . Pings the backend health endpoint + * on mount to drive the API status indicator. + */ +export default function AppLayout() { + const [collapsed, setCollapsed] = useState(false); + const [apiStatus, setApiStatus] = useState('checking'); + + useEffect(() => { + let active = true; + checkHealth().then((ok) => { + if (active) setApiStatus(ok ? 'connected' : 'disconnected'); + }); + return () => { + active = false; + }; + }, []); + + return ( + + setCollapsed((c) => !c)} + apiStatus={apiStatus} + /> + + + + + + + ); +} diff --git a/frontend/src/components/Layout/Sidebar.tsx b/frontend/src/components/Layout/Sidebar.tsx new file mode 100644 index 0000000..b4c856b --- /dev/null +++ b/frontend/src/components/Layout/Sidebar.tsx @@ -0,0 +1,141 @@ +import { + Box, + Drawer, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Toolbar, + Tooltip, + Typography, +} from '@mui/material'; +import HubIcon from '@mui/icons-material/Hub'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import { navItems } from './navConfig'; + +const EXPANDED_WIDTH = 240; +const COLLAPSED_WIDTH = 64; + +interface SidebarProps { + collapsed: boolean; +} + +/** + * Dark navigation drawer, Azure-Portal style. Highlights the active route and + * collapses to an icon-only rail when `collapsed` is true. + */ +export default function Sidebar({ collapsed }: SidebarProps) { + const location = useLocation(); + const navigate = useNavigate(); + const width = collapsed ? COLLAPSED_WIDTH : EXPANDED_WIDTH; + + const isActive = (path: string) => + path === '/' + ? location.pathname === '/' + : location.pathname.startsWith(path); + + return ( + + + + {!collapsed && ( + + MEP + + )} + + + + + {navItems.map((item) => { + const active = isActive(item.path); + return ( + + + navigate(item.path)} + sx={{ + minHeight: 44, + justifyContent: collapsed ? 'center' : 'initial', + px: 2.5, + color: active ? '#FFFFFF' : '#C8C6C4', + borderLeft: active + ? '3px solid #2B88D8' + : '3px solid transparent', + '&.Mui-selected': { + backgroundColor: 'rgba(43,136,216,0.16)', + }, + '&.Mui-selected:hover': { + backgroundColor: 'rgba(43,136,216,0.24)', + }, + '&:hover': { + backgroundColor: 'rgba(255,255,255,0.06)', + }, + }} + > + + {item.icon} + + {!collapsed && ( + + )} + + + + ); + })} + + + + ); +} + +export { EXPANDED_WIDTH, COLLAPSED_WIDTH }; diff --git a/frontend/src/components/Layout/TopBar.tsx b/frontend/src/components/Layout/TopBar.tsx new file mode 100644 index 0000000..d039e4a --- /dev/null +++ b/frontend/src/components/Layout/TopBar.tsx @@ -0,0 +1,62 @@ +import { AppBar, Box, IconButton, Toolbar, Tooltip, Typography } from '@mui/material'; +import MenuIcon from '@mui/icons-material/Menu'; +import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'; + +type ApiStatus = 'checking' | 'connected' | 'disconnected'; + +interface TopBarProps { + onToggleSidebar: () => void; + apiStatus: ApiStatus; +} + +const statusMeta: Record = { + checking: { color: '#8A8886', label: 'Checking API…' }, + connected: { color: '#107C10', label: 'API Connected' }, + disconnected: { color: '#D13438', label: 'API Disconnected' }, +}; + +/** Top app bar: menu toggle, product title, and live API status indicator. */ +export default function TopBar({ onToggleSidebar, apiStatus }: TopBarProps) { + const meta = statusMeta[apiStatus]; + + return ( + t.zIndex.drawer + 1 }}> + + + + + + + Migration Evaluation Platform + + + + + + + {meta.label} + + + + + + ); +} + +export type { ApiStatus }; diff --git a/frontend/src/components/Layout/navConfig.tsx b/frontend/src/components/Layout/navConfig.tsx new file mode 100644 index 0000000..2c847fe --- /dev/null +++ b/frontend/src/components/Layout/navConfig.tsx @@ -0,0 +1,25 @@ +import type { ReactNode } from 'react'; +import DashboardIcon from '@mui/icons-material/Dashboard'; +import StorageIcon from '@mui/icons-material/Storage'; +import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutlineOutlined'; +import FactCheckIcon from '@mui/icons-material/FactCheck'; +import AssessmentIcon from '@mui/icons-material/Assessment'; +import HistoryIcon from '@mui/icons-material/History'; +import SettingsIcon from '@mui/icons-material/Settings'; + +export interface NavItem { + label: string; + path: string; + icon: ReactNode; +} + +/** Sidebar navigation items, in display order. */ +export const navItems: NavItem[] = [ + { label: 'Dashboard', path: '/', icon: }, + { label: 'Migration Runs', path: '/migration-runs', icon: }, + { label: 'New Migration', path: '/new-migration', icon: }, + { label: 'Validation', path: '/validation', icon: }, + { label: 'Reports', path: '/reports', icon: }, + { label: 'History', path: '/history', icon: }, + { label: 'Administration', path: '/administration', icon: }, +]; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..bd77e4b --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,23 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import { ThemeProvider, CssBaseline } from '@mui/material'; + +import '@fontsource/roboto/300.css'; +import '@fontsource/roboto/400.css'; +import '@fontsource/roboto/500.css'; +import '@fontsource/roboto/700.css'; + +import App from './App'; +import theme from './theme'; + +createRoot(document.getElementById('root')!).render( + + + + + + + + , +); diff --git a/frontend/src/pages/Administration.tsx b/frontend/src/pages/Administration.tsx new file mode 100644 index 0000000..b377fff --- /dev/null +++ b/frontend/src/pages/Administration.tsx @@ -0,0 +1,12 @@ +import SettingsIcon from '@mui/icons-material/Settings'; +import PagePlaceholder from './PagePlaceholder'; + +export default function Administration() { + return ( + } + description="Manage connections, users, and platform configuration settings." + /> + ); +} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx new file mode 100644 index 0000000..00cdb80 --- /dev/null +++ b/frontend/src/pages/Dashboard.tsx @@ -0,0 +1,12 @@ +import DashboardIcon from '@mui/icons-material/Dashboard'; +import PagePlaceholder from './PagePlaceholder'; + +export default function Dashboard() { + return ( + } + description="An at-a-glance overview of migration activity, health, and key metrics will appear here." + /> + ); +} diff --git a/frontend/src/pages/History.tsx b/frontend/src/pages/History.tsx new file mode 100644 index 0000000..c1b20d9 --- /dev/null +++ b/frontend/src/pages/History.tsx @@ -0,0 +1,12 @@ +import HistoryIcon from '@mui/icons-material/History'; +import PagePlaceholder from './PagePlaceholder'; + +export default function History() { + return ( + } + description="Audit the full history of migration and validation activity over time." + /> + ); +} diff --git a/frontend/src/pages/MigrationRuns.tsx b/frontend/src/pages/MigrationRuns.tsx new file mode 100644 index 0000000..cf91133 --- /dev/null +++ b/frontend/src/pages/MigrationRuns.tsx @@ -0,0 +1,12 @@ +import StorageIcon from '@mui/icons-material/Storage'; +import PagePlaceholder from './PagePlaceholder'; + +export default function MigrationRuns() { + return ( + } + description="Browse and monitor active and completed CSV → PostgreSQL migration runs." + /> + ); +} diff --git a/frontend/src/pages/NewMigration.tsx b/frontend/src/pages/NewMigration.tsx new file mode 100644 index 0000000..e485574 --- /dev/null +++ b/frontend/src/pages/NewMigration.tsx @@ -0,0 +1,12 @@ +import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutlineOutlined'; +import PagePlaceholder from './PagePlaceholder'; + +export default function NewMigration() { + return ( + } + description="Configure and launch a new migration by uploading source data and selecting a target." + /> + ); +} diff --git a/frontend/src/pages/PagePlaceholder.tsx b/frontend/src/pages/PagePlaceholder.tsx new file mode 100644 index 0000000..94a93b6 --- /dev/null +++ b/frontend/src/pages/PagePlaceholder.tsx @@ -0,0 +1,49 @@ +import type { ReactNode } from 'react'; +import { Box, Card, CardContent, Chip, Typography } from '@mui/material'; + +interface PagePlaceholderProps { + title: string; + icon: ReactNode; + description?: string; +} + +/** + * Empty-state card used by every route until real functionality lands. + * Keeps the shell looking finished while signalling work-in-progress. + */ +export default function PagePlaceholder({ + title, + icon, + description, +}: PagePlaceholderProps) { + return ( + + + {icon} + {title} + + + + + + {icon} + + + + {title} — Coming Soon + + + {description ?? + 'This section is part of the Migration Evaluation Platform and will be available in an upcoming release.'} + + + + + ); +} diff --git a/frontend/src/pages/Reports.tsx b/frontend/src/pages/Reports.tsx new file mode 100644 index 0000000..5b2625e --- /dev/null +++ b/frontend/src/pages/Reports.tsx @@ -0,0 +1,12 @@ +import AssessmentIcon from '@mui/icons-material/Assessment'; +import PagePlaceholder from './PagePlaceholder'; + +export default function Reports() { + return ( + } + description="Generate and export detailed evaluation reports for each migration run." + /> + ); +} diff --git a/frontend/src/pages/Validation.tsx b/frontend/src/pages/Validation.tsx new file mode 100644 index 0000000..bad2d94 --- /dev/null +++ b/frontend/src/pages/Validation.tsx @@ -0,0 +1,12 @@ +import FactCheckIcon from '@mui/icons-material/FactCheck'; +import PagePlaceholder from './PagePlaceholder'; + +export default function Validation() { + return ( + } + description="Review data-quality checks and validation assertions against migrated datasets." + /> + ); +} diff --git a/frontend/src/theme.ts b/frontend/src/theme.ts new file mode 100644 index 0000000..ac08916 --- /dev/null +++ b/frontend/src/theme.ts @@ -0,0 +1,81 @@ +import { createTheme } from '@mui/material/styles'; + +/** + * MEP theme — light, clean, professional. Modeled on the Azure Portal: + * Azure blue primary (#0078D4), neutral greys, subtle borders, and a + * comfortable density for an enterprise SaaS product. + */ +const theme = createTheme({ + palette: { + mode: 'light', + primary: { + main: '#0078D4', // Azure blue + dark: '#005A9E', + light: '#2B88D8', + contrastText: '#FFFFFF', + }, + secondary: { + main: '#5C2D91', + }, + success: { + main: '#107C10', + }, + error: { + main: '#D13438', + }, + background: { + default: '#F3F2F1', // Azure Portal content grey + paper: '#FFFFFF', + }, + text: { + primary: '#201F1E', + secondary: '#605E5C', + }, + divider: '#EDEBE9', + }, + typography: { + fontFamily: + '"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', + h4: { fontWeight: 600 }, + h5: { fontWeight: 600 }, + h6: { fontWeight: 600 }, + subtitle1: { fontWeight: 600 }, + button: { textTransform: 'none', fontWeight: 600 }, + }, + shape: { + borderRadius: 4, + }, + components: { + MuiAppBar: { + styleOverrides: { + root: { + backgroundColor: '#FFFFFF', + color: '#201F1E', + boxShadow: 'none', + borderBottom: '1px solid #EDEBE9', + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + backgroundImage: 'none', + }, + }, + }, + MuiListItemButton: { + styleOverrides: { + root: { + '&.Mui-selected': { + backgroundColor: 'rgba(255,255,255,0.12)', + }, + '&.Mui-selected:hover': { + backgroundColor: 'rgba(255,255,255,0.18)', + }, + }, + }, + }, + }, +}); + +export default theme; diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..6830b6f --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..8455dcb --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..e3932ee --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + proxy: { + // Proxy API calls to the FastAPI backend during local development. + // In production, nginx handles this routing (see Dockerfile). + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + }, + }, + }, +}); diff --git a/uploads/.gitkeep b/uploads/.gitkeep new file mode 100644 index 0000000..e69de29