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). + +[-blueviolet)](#) +[](#) +[](#) +[](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 @@ + + +
+ + + +