mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-22 23:18:05 +03:00
rebel shell
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"iidx-sp","userID":1,"sessionID":"Q578fa997a13b92203dbc59082a452636631ff691","type":"Created","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":0},"timestamp":1778174638000,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qc252f411274450ce5698af2907997cbab60105d2","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":0},"timestamp":1778174660089,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q5d5e46543de37de7144aabde86aaeaf00498195a","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":1},"timestamp":1778174660094,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qc37df566339572b6fee3ca5edd4b3bf0314e29c5","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":2},"timestamp":1778174660099,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q8cbcd83ef2ee60cecb791c09e573cacb82614819","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":3},"timestamp":1778174660105,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q189cf02b87c556a9b3f871e303758dd88d16c96a","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":4},"timestamp":1778174660108,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qe40bb6742d397db1bfedc9aa2da386994054cb8e","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":5},"timestamp":1778174660112,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q7d9e5a5aa58b424f33253e67721fb416d43c0a8b","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":6},"timestamp":1778174660115,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qcafb063529a7746b14be61b5f8b8f785b4c0bb9d","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":7},"timestamp":1778174660121,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qdde44db5089244cc55f662c6a41ffb2fb714a8b8","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":8},"timestamp":1778174660131,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q6e8d8cbad5078faf2b592504808d09e213ed0129","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":9},"timestamp":1778174660136,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q8559d0c86bf3b2f8b15801d36f27fb876ea552b3","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":10},"timestamp":1778174660142,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qe37e28c4561537d764d26d3084f544d4f07fc794","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":11},"timestamp":1778174660147,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q6657d8805d106093b0dddb14cb504b937f132760","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":12},"timestamp":1778174660155,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q48f3114dd0c29c80252fd65a670d97fa4a600f82","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":13},"timestamp":1778174660162,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q990c934c1383480e3d37baf91bd6a765cf8b3a2a","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":14},"timestamp":1778174660165,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q7a417c5a2159bf29e7a01e5250a0711528697f8f","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":15},"timestamp":1778174660170,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q7e1c4f468c77e2fefc8f7c6883e2de759bdd25b9","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":16},"timestamp":1778174660174,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qee252e93a7d251e87e9a7fe6c17139b2d2f99e7a","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":17},"timestamp":1778174660178,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qa105eb34d8423b192e43c238f8ff91083e1bffc9","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":18},"timestamp":1778174660181,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q27daaebb4468a50aae51882418658cc1aa106a34","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":19},"timestamp":1778174660184,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q8c121fa8b73857c8ee117e700842b866e3baf6a7","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":20},"timestamp":1778174660199,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q7210852aa9d6b82fe8d99da03f02d86782285985","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":21},"timestamp":1778174660205,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qcfb1824558a1a99b1538dbd7432289a8be57b3e6","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":22},"timestamp":1778174660209,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q49efff3408ecd0c5b7e41548875f8e443d0dd7f9","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":23},"timestamp":1778174660217,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qc2618e82e8d9911531d1020c4d467859c9214b36","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":24},"timestamp":1778174660223,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q0f170b6b312d00be302e5cc0d4285da541524fbe","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":25},"timestamp":1778174660227,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qdb008e8c2209f91ad53ec0cdee933f67f6e5331e","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":26},"timestamp":1778174660233,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q045d94351296c45a1eaabae9a30029dc68318e19","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":27},"timestamp":1778174660236,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q39b379df7a10f4a72574f3680b52a7ef69751af4","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":28},"timestamp":1778174660245,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q811a4764a3d2534fb4b1da6cbaea1d407a60d0b5","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":29},"timestamp":1778174660248,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qb79f15ac66e1965c35be2004006e577e071b87b7","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":30},"timestamp":1778174660252,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q0f4611dfe6752e90e0ea7ad0a339aed51391d94a","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":31},"timestamp":1778174660257,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q9f0a302cbb6160468da3399669832c52ae7c5910","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":32},"timestamp":1778174660260,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q9418f31ea6a52ffaca93f0657abab15f648d3f24","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":33},"timestamp":1778174660263,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qb4db5061c31ee7743fd4c592a3136cee6c53225d","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":34},"timestamp":1778174660267,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qa7a6babb5c27931e45e0396d81c95272beee00f8","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":35},"timestamp":1778174660270,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q927681faa73a41ffbcc2b2e51bb3220656f7a27c","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":36},"timestamp":1778174660278,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q1edafb103d7f842bd3164bf2d77cca323ddc24d5","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":37},"timestamp":1778174660287,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qc93f8402727171169df28aa12c1852e59a00b0b4","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":38},"timestamp":1778174660294,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qae0736c538299e163942c0fd4fd5c34bcd8ab7c5","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":39},"timestamp":1778174660299,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qa8723107d169ea4e765d28d9dec764e119d4879e","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":40},"timestamp":1778174660303,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qe8e555ad077e1897eab4ece65580c27f1fd8b0bb","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":41},"timestamp":1778174660309,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qfef6ca0ce28485e788d6562db2527b7a8ddce498","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":42},"timestamp":1778174660314,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q5c68d2fe24dc80dfd015b2abcb89e4eba774feaf","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":43},"timestamp":1778174660317,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q4d88a6581e28b7024f020673feb45b75c1d101b3","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":44},"timestamp":1778174660321,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q7d8887d5d3fd265b73b1b29363d305ce13360c94","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":45},"timestamp":1778174660327,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q6f0fb12cb6e8aaf252a1d6040e00d391ea8464d4","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":46},"timestamp":1778174660345,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qd4ec361100e6590f313350b9e3e9150d6387e2ba","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":47},"timestamp":1778174660348,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qe0381db5230e8b7dd310bf2d23d8427dbbb84822","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":48},"timestamp":1778174660350,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q31e15ba0f00783be89720b1aec46550b58976d3b","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":49},"timestamp":1778174660354,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qba5cc5abba6d6f03e6868628b3a4de1461abbc4c","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":50},"timestamp":1778174660360,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qc9d38a91243fdd685b83f61292103dc53da0d4cf","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":51},"timestamp":1778174660365,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q8dabfc9ce6038eb05610efabd1bbe169fe060c46","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":52},"timestamp":1778174660369,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q44c21d76c1f0aceca460bbd6dd9f32e3ba5c7732","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":53},"timestamp":1778174660372,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qba93137b42cdac7a6a33e6b0b8936879e526089c","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":54},"timestamp":1778174660378,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qac474a57764cbaeb6b055f0c98d12bd1dfc75497","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":55},"timestamp":1778174660383,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q54aed62223d5f4a774bbf7283e1d71bffa6a8877","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":56},"timestamp":1778174660389,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qed26c346b3a65815a31b5228e4e632f42f62fa39","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":57},"timestamp":1778174660401,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qaf7a4851b6f7ddf6e9c186c8418b6130bad70798","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":58},"timestamp":1778174660405,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q801affd326b159befcc5ec230f6eac3149603622","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":59},"timestamp":1778174660415,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qc69f6c9c7ab8ecdbcad074aab2f796f052f5d0f8","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":60},"timestamp":1778174660430,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q8b639b6300480f0ecfd5db79b36d2e3c132a54ac","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":61},"timestamp":1778174660438,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qf85158125f1aeaea09a2827da2b3d7d45aba72bc","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":62},"timestamp":1778174660443,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qd9ea6889198ee5e8a099baa6c91bcffa7eca95c3","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":63},"timestamp":1778174660449,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q0585eab487fb017863aaac9f8768e9d1b72cc123","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":64},"timestamp":1778174660453,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q567de464814f23d575e9004f505e1a322be5de31","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":65},"timestamp":1778174660456,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q0b7df3b5afa5e0b50e42c67639b43f175072ce22","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":66},"timestamp":1778174660462,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qdc9e3f944ec4759285e25a25c7a372303c7e18b9","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":67},"timestamp":1778174660465,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q38e82752ad57d923af66a12471ade3d9b883f958","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":68},"timestamp":1778174660469,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q452773b20751ea073659360a00cec1b3861b85ef","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":69},"timestamp":1778174660474,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qd55da19de534bd8038ed3e52b82a890567b6ad19","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":70},"timestamp":1778174660482,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q314a9e746a8b962a00703e9d84a93190c0e915f3","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":71},"timestamp":1778174660487,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qfa374369968d49856bca05e82f36b4d7145ed1ec","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":72},"timestamp":1778174660490,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q76b0603b5a47f4a0236c9087dfbbfeba1fb3bc7b","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":73},"timestamp":1778174660506,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q203f0fe1b402e9a0448ac39e6cd1db01d511a50a","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":74},"timestamp":1778174660514,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q8c27ac1b60300274df3d669847921d35d01289ae","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":75},"timestamp":1778174660520,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q25b244ba922b0d4f1092bf419ea9788b66708f7c","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":76},"timestamp":1778174660524,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q70d5b9a4d9b79fff4f66b8ae300cf64fc488f02a","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":77},"timestamp":1778174660531,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q20e1e28b721c3b0733cd924e720704962e54e3e8","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":78},"timestamp":1778174660553,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qadc97ca77718ccfb8bde7ac9593edaac4bb87a6f","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":79},"timestamp":1778174660569,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q4b2feb98be77e6882f84d6a9cbeca42887abdbf3","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":80},"timestamp":1778174660573,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q77b97d7a7b1bfda84da3f472cc32bacec267219f","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":81},"timestamp":1778174660581,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q424e90d547303ea4923e0258cfc2d8d392679cae","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":82},"timestamp":1778174660586,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q97f2bc192eeb70752c9e30cfe7dcde99bc47e690","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":83},"timestamp":1778174660590,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q59001766f9b8a52c8dcc97bf1d042b454708eae4","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":84},"timestamp":1778174660593,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q00e706d002c2e9ea6086371b935a50b282706d39","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":85},"timestamp":1778174660598,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q0086ff797f2b95359b9472c107f20ae075898b21","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":86},"timestamp":1778174660601,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qe989edaca5a2f24b8d604bf98ad03a48baf0f010","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":87},"timestamp":1778174660606,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q12ef1db3064ea85095e2ff9d74a45a21d7ccc9b5","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":88},"timestamp":1778174660622,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q660bd3bb616c87eeaa3df3b65f25f1a98c57af10","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":89},"timestamp":1778174660626,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q1d038349e88718833f4acd084ca6940a66deb9c4","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":90},"timestamp":1778174660636,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q11339defac1468d048e2b57b1b4d2cc67751ac86","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":91},"timestamp":1778174660639,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q44a76aab9debe86da79c4b870f83d88f76dc4118","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":92},"timestamp":1778174660642,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qe1adc2f7ec768f394813223ed21bd2b571969a4a","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":93},"timestamp":1778174660652,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q489661edfc15cfc92c3834eceb309f1e2d4bb893","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":94},"timestamp":1778174660660,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q489661edfc15cfc92c3834eceb309f1e2d4bb893","type":"Appended","repeatSessionInThisLoad":true,"willPush":false,"runId":"post-fix","priorReturnCount":95},"timestamp":1778174660671,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q06014b95faecab25d0f47b051f1e38d96af5ea7d","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":95},"timestamp":1778174660680,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qdcf58e13a815753fe75b38a564c8e581fef42d17","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":96},"timestamp":1778174660684,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q016e78c91f15ef13016dedc24a16c91284de26ba","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":97},"timestamp":1778174660688,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q12b6a4887a80ecc13a74079376f1259c5cc40e86","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":98},"timestamp":1778174660690,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q577397051d7966d96c1fb5cf649654f3b8dd6edb","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":99},"timestamp":1778174660699,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qa0b0c7a4bb6862f33febafdc80de2efe2d2adb2d","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":100},"timestamp":1778174660702,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qa17b785caf6e37dc996afe0f218baa008a63fb1d","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":101},"timestamp":1778174660705,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qa5bac01b00d12082158013a264ae8c2b10c837c6","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":102},"timestamp":1778174660708,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q6c6e8a526936a57adab446b28d870ae68bee3910","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":103},"timestamp":1778174660715,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q190d862f7853a94510ae603cefa8fec2d0010c17","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":104},"timestamp":1778174660719,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q68b4212ad3a72c9029272f3c8dba2909f6c717d5","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":105},"timestamp":1778174660724,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q61e5881c2e33e7b9c47afc832e54fa23c07657a3","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":106},"timestamp":1778174660729,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q151c94c4fb26eab70ff8db30652dac361d09d11b","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":107},"timestamp":1778174660736,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qb19bcd6ae2f4c2b1bee880fd6021214bfbf05604","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":108},"timestamp":1778174660741,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q952a8c756b74a7c556f19edb77628dbb04c0800b","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":109},"timestamp":1778174660744,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q673e224863b4bd6073c46591576644da16c4324f","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":110},"timestamp":1778174660750,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qd814902d29a9e34622ddc9dfa5089dc8c57f80bc","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":111},"timestamp":1778174660754,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q3ebba12414b002bbde0e9f5ca58134ca37071867","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":112},"timestamp":1778174660758,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qd073fc8da813e7fefa8fe1d6352714f48dd0ff7a","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":113},"timestamp":1778174660761,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q401bdb009b493618518bd7c4a066bd54acdabc10","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":114},"timestamp":1778174660768,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q2ce7b164c51b3bf0e4dcfd4b9b2228892815b856","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":115},"timestamp":1778174660772,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q0f405f56021d7115617fde39cadcaf6b89866705","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":116},"timestamp":1778174660778,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q9b7187fe54b150ef7e249c75257cd2b252084e85","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":117},"timestamp":1778174660797,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q70dc4b3da3675781ee314b6f44f95187cba7d3c1","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":118},"timestamp":1778174660809,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q60b1a04f2e28aab22c166378e89871125de6d563","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":119},"timestamp":1778174660813,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q7fc3d41514a6629a92e0c89674848e4224fed28d","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":120},"timestamp":1778174660818,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q4d84680f2603292d7f650921e4fdea73b0a6c6ca","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":121},"timestamp":1778174660822,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q3d517c038e36209ee4256306c34690fd53a8306e","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":122},"timestamp":1778174660826,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qaa50b6fadf96a3e12d60cca87f87cb5f7f462c69","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":123},"timestamp":1778174660843,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qc769036b5a38cb57c5ee1bf6f17360ed47f5ea54","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":124},"timestamp":1778174660858,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q84b63c6eefe648254d00a5ec71dc3808339dcce2","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":125},"timestamp":1778174660862,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q7a1ed0017220e8b96e861fa6289129b7e7f0cb33","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":126},"timestamp":1778174660867,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q86e26421f03118913a2f7a9adf630a3b10c879f9","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":127},"timestamp":1778174660873,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q7554af752b6d25d731396bdb31432e41e77ac694","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":128},"timestamp":1778174660877,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q100d03555b2f9ffbc2a9da5bb2e973b75356f9e7","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":129},"timestamp":1778174660883,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q9a42fb36c8479ca5c84e5d7c9e9d5103ae61e8fb","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":130},"timestamp":1778174660888,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q935ab1b2ff13b6a00f83bff9dc92e1b816eb8c98","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":131},"timestamp":1778174660893,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q97c8f2cf1053a36e4966d82265ecea04d1051f1e","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":132},"timestamp":1778174660904,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q5ded6f7f8d7f5d37223f280f2d900da118f7a193","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":133},"timestamp":1778174660908,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q7d5db734e4edef4477ef4cab6b6427fb9f45e4a8","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":134},"timestamp":1778174660911,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q948fd05d2c9ed5d49aa75daace5658c6a64aa74c","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":135},"timestamp":1778174660919,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q10363461fbf6a6be6dd31c1f1e978b0c0a9c08a9","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":136},"timestamp":1778174660923,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Q55b15f2d894f8e3bcd768ed1510da91c2158992f","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":137},"timestamp":1778174660928,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qb87640a3371880e254627383c3b23cf5230527c9","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":138},"timestamp":1778174660936,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"sessions.ts:LoadScoresIntoSessions:beforePush","message":"sessionInfoReturn push","data":{"game":"jubeat","userID":1,"sessionID":"Qf199ca3f596016185da0299200f1c4322656cce1","type":"Appended","repeatSessionInThisLoad":false,"willPush":true,"runId":"post-fix","priorReturnCount":139},"timestamp":1778174660941,"hypothesisId":"H1"}
|
||||
{"sessionId":"7b27d8","location":"finalize-import-pg.ts:finalizeImportToPostgres:entry","message":"finalizeImportToPostgres called","data":{"importID":"4584b4feb70692dba1707a42c243a56a4b3be56b","runId":"post-fix"},"timestamp":1778174661244,"hypothesisId":"H2"}
|
||||
{"sessionId":"7b27d8","location":"finalize-import-pg.ts:import_session:preInsert","message":"import_session insert payload stats","data":{"importID":"4584b4feb70692dba1707a42c243a56a4b3be56b","createdLen":140,"uniqueSessionCount":140,"dupSessionIds":[],"runId":"post-fix"},"timestamp":1778174661246,"hypothesisId":"H1"}
|
||||
@@ -9,7 +9,7 @@ alwaysApply: false
|
||||
In Kysely `select`, `where`, `orderBy`, `join` conditions, and raw `sql` fragments, **always** qualify column names with the table or join alias. Do not use bare column names when the query references a table (including single-table queries, for consistency and safer refactors).
|
||||
|
||||
```typescript
|
||||
// ❌ BAD — ambiguous once joins or CTEs are added; harder to grep
|
||||
// ❌ BAD - ambiguous once joins or CTEs are added; harder to grep
|
||||
.select(["data", "chart_id", "user_id"])
|
||||
.where("id", "=", id)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ name: actions-and-pg-migration
|
||||
description: Patterns for writing actions (MakeAction/MakeAnonAction), using the Postgres DB (Kysely), and migrating Express routes from MongoDB to Postgres in the Tachi server. Use when adding a new mutation, migrating a Mongo-backed router to Postgres, writing action files, or writing tests for actions or routers.
|
||||
---
|
||||
|
||||
# Actions & Postgres Migration — Tachi Server
|
||||
# Actions & Postgres Migration - Tachi Server
|
||||
|
||||
## Actions
|
||||
|
||||
@@ -31,9 +31,9 @@ import { ExpectedErr } from "bliss";
|
||||
export const ACTION_MyAction = MakeAction(
|
||||
"MY_ACTION",
|
||||
async (taker, { fieldA, fieldB }) => {
|
||||
// taker.acct.id — authenticated user's numeric ID
|
||||
// taker.acct.username — their username
|
||||
// taker.ip — request IP (for audit log)
|
||||
// taker.acct.id - authenticated user's numeric ID
|
||||
// taker.acct.username - their username
|
||||
// taker.ip - request IP (for audit log)
|
||||
|
||||
if (somethingWrong) {
|
||||
throw new ExpectedErr(400, "Human-readable reason.");
|
||||
@@ -50,18 +50,18 @@ export const ACTION_MyAction = MakeAction(
|
||||
|
||||
### ExpectedErr
|
||||
|
||||
`ExpectedErr(code, reason)` is an intentional control-flow error. Throw it for 400/403/404/409 etc. The global Express error handler in `server.ts` (`MAIN_ERR_HANDLER`) catches it and returns `{ success: false, description: reason }` with the right HTTP status. **Never wrap action calls in `try/catch`** — let it propagate.
|
||||
`ExpectedErr(code, reason)` is an intentional control-flow error. Throw it for 400/403/404/409 etc. The global Express error handler in `server.ts` (`MAIN_ERR_HANDLER`) catches it and returns `{ success: false, description: reason }` with the right HTTP status. **Never wrap action calls in `try/catch`** - let it propagate.
|
||||
|
||||
### Calling actions from a router
|
||||
|
||||
```typescript
|
||||
// taker construction — same pattern everywhere:
|
||||
// taker construction - same pattern everywhere:
|
||||
const user = req.session.tachi?.user;
|
||||
if (!user) return res.status(401).json({ success: false, description: "..." });
|
||||
|
||||
const taker = { ip: req.ip, acct: { id: user.id, username: user.username } };
|
||||
|
||||
// No try/catch — errors reach MAIN_ERR_HANDLER automatically
|
||||
// No try/catch - errors reach MAIN_ERR_HANDLER automatically
|
||||
const result = await ACTION_MyAction(taker, { fieldA: body.fieldA });
|
||||
|
||||
return res.status(200).json({ success: true, description: "...", body: result });
|
||||
@@ -123,7 +123,7 @@ await DB.selectFrom("priv_api_client")
|
||||
// Insert
|
||||
await DB.insertInto("priv_api_client").values({ ... }).execute();
|
||||
|
||||
// Update (fetch after separately — don't use .returning() with prefixed column lists)
|
||||
// Update (fetch after separately - don't use .returning() with prefixed column lists)
|
||||
await DB.updateTable("priv_api_client").set({ name: "New" }).where("client_id", "=", id).execute();
|
||||
const updated = await GetClientByID(id); // re-fetch via existing query helper
|
||||
|
||||
@@ -146,12 +146,12 @@ Existing query helpers (e.g. `GetClientByID`) live in `src/utils/queries/`. Use
|
||||
### Checklist
|
||||
|
||||
1. **Identify Mongo collections** → find the equivalent `priv_*` Postgres table.
|
||||
2. **Reads** — replace `MONGODB_KILL["collection"].find/findOne()` with Kysely selects. Use the existing `SELECT_*` + `To*Document` helpers from `src/lib/db-formats/`.
|
||||
3. **Mutations** — extract each write operation into a `MakeAction`-wrapped file in `src/actions/`. Add its signature to `ActionSignatures` in `actions.ts`.
|
||||
4. **Middleware** — update any middleware that does Mongo lookups (e.g. `GetClientFromID`) to use a query helper or direct Kysely query.
|
||||
5. **Remove Mongo import** — `MONGODB_KILL` should be gone from the file.
|
||||
6. **Ownership checks** — move them inside the action (`taker.acct.id === row.author`) rather than in Express middleware, so the action is self-contained.
|
||||
7. **Error handler** — throw `ExpectedErr` instead of returning early; no `try/catch` in routes needed.
|
||||
2. **Reads** - replace `MONGODB_KILL["collection"].find/findOne()` with Kysely selects. Use the existing `SELECT_*` + `To*Document` helpers from `src/lib/db-formats/`.
|
||||
3. **Mutations** - extract each write operation into a `MakeAction`-wrapped file in `src/actions/`. Add its signature to `ActionSignatures` in `actions.ts`.
|
||||
4. **Middleware** - update any middleware that does Mongo lookups (e.g. `GetClientFromID`) to use a query helper or direct Kysely query.
|
||||
5. **Remove Mongo import** - `MONGODB_KILL` should be gone from the file.
|
||||
6. **Ownership checks** - move them inside the action (`taker.acct.id === row.author`) rather than in Express middleware, so the action is self-contained.
|
||||
7. **Error handler** - throw `ExpectedErr` instead of returning early; no `try/catch` in routes needed.
|
||||
|
||||
### Permission columns
|
||||
|
||||
|
||||
@@ -36,9 +36,9 @@ Implementation: workspace package `typescript/coverage-tools` (`bun run --filter
|
||||
|
||||
Edit `typescript/coverage-tools/src/manifest.ts`: add a `CoverageSource` with:
|
||||
|
||||
- `id` — short name for `--packages`
|
||||
- `packageRoot` — e.g. `typescript/my-pkg`
|
||||
- `coverageFinal` — path to `coverage/coverage-final.json` relative to repo root (Vitest default unless `coverage.reportsDirectory` overrides)
|
||||
- `id` - short name for `--packages`
|
||||
- `packageRoot` - e.g. `typescript/my-pkg`
|
||||
- `coverageFinal` - path to `coverage/coverage-final.json` relative to repo root (Vitest default unless `coverage.reportsDirectory` overrides)
|
||||
|
||||
## Programmatic use
|
||||
|
||||
@@ -50,6 +50,6 @@ import { buildReport, COVERAGE_SOURCES } from "tachi-coverage-tools";
|
||||
|
||||
## Related files
|
||||
|
||||
- `typescript/coverage-tools/src/manifest.ts` — source registry
|
||||
- `typescript/coverage-tools/src/report-coverage.ts` — CLI flags and `buildReport`
|
||||
- `Justfile-test` — `coverage-report` recipe
|
||||
- `typescript/coverage-tools/src/manifest.ts` - source registry
|
||||
- `typescript/coverage-tools/src/report-coverage.ts` - CLI flags and `buildReport`
|
||||
- `Justfile-test` - `coverage-report` recipe
|
||||
|
||||
@@ -23,8 +23,8 @@ description: Guidance on using db-formats/ column lists (SELECT_*) and document
|
||||
|
||||
Every Postgres table that is returned through the API needs two things in `src/lib/db-formats/<table>.ts`:
|
||||
|
||||
1. **`SELECT_*`** — a `const` array of `"table.column"` strings listing exactly the columns needed
|
||||
2. **`To*Document`** — a mapper function that converts a typed Kysely row to the matching `tachi-common` document type
|
||||
1. **`SELECT_*`** - a `const` array of `"table.column"` strings listing exactly the columns needed
|
||||
2. **`To*Document`** - a mapper function that converts a typed Kysely row to the matching `tachi-common` document type
|
||||
|
||||
```typescript
|
||||
// src/lib/db-formats/my-table.ts
|
||||
@@ -47,7 +47,7 @@ export function ToMyDocument(
|
||||
}
|
||||
```
|
||||
|
||||
The `Selection<Database, "table", (typeof SELECT_*)[number]>` type ensures the mapper only receives columns that were actually selected — Kysely will error at compile time if the select array and mapper fall out of sync.
|
||||
The `Selection<Database, "table", (typeof SELECT_*)[number]>` type ensures the mapper only receives columns that were actually selected - Kysely will error at compile time if the select array and mapper fall out of sync.
|
||||
|
||||
---
|
||||
|
||||
@@ -116,7 +116,7 @@ const { clientSecret: _secret, ...publicDoc } = ToAPIClientDocument(row);
|
||||
3. Export `To<Entity>Document` typed with `Selection<Database, "table", (typeof SELECT_<TABLE>)[number]>`
|
||||
4. Return the matching `tachi-common` document type (import from `"tachi-common"`)
|
||||
5. Apply all naming/shape conventions from the table above
|
||||
6. Import and use `SELECT_*` + `To*Document` in the router/util — never `.selectAll()`
|
||||
6. Import and use `SELECT_*` + `To*Document` in the router/util - never `.selectAll()`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ default: {
|
||||
}
|
||||
```
|
||||
|
||||
Reference: `staticAssertUnreachable` in `typescript/server/src/utils/misc.ts` — it takes `never` and throws at runtime if execution reaches it.
|
||||
Reference: `staticAssertUnreachable` in `typescript/server/src/utils/misc.ts` - it takes `never` and throws at runtime if execution reaches it.
|
||||
|
||||
## Why
|
||||
|
||||
@@ -47,13 +47,13 @@ Reference: `staticAssertUnreachable` in `typescript/server/src/utils/misc.ts`
|
||||
|
||||
## Avoid
|
||||
|
||||
- A `default` that only `throw new Error("unreachable")` **without** passing a `never`-typed value — you lose exhaustiveness checking.
|
||||
- `default: break` or empty `default` on switches that are supposed to be exhaustive — same problem.
|
||||
- A `default` that only `throw new Error("unreachable")` **without** passing a `never`-typed value - you lose exhaustiveness checking.
|
||||
- `default: break` or empty `default` on switches that are supposed to be exhaustive - same problem.
|
||||
|
||||
## When not to use
|
||||
|
||||
- Switches that are **intentionally partial** (e.g. only handle some cases and fall through to shared logic). Use an explicit `default` that handles “everything else” with correct typing, not `staticAssertUnreachable`.
|
||||
- If the discriminant is typed too widely (e.g. `string`), `default` will not be `never` and the pattern will not typecheck — narrow the type first or handle the real domain of values.
|
||||
- If the discriminant is typed too widely (e.g. `string`), `default` will not be `never` and the pattern will not typecheck - narrow the type first or handle the real domain of values.
|
||||
|
||||
## Imports
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ name: mongo-migration-constraints
|
||||
description: Migrates server code from MongoDB to Postgres without modifying the legacy Mongo service. Never edits typescript/server/src/services/mongo/ (especially db.ts). Requires removing MONGODB_KILL imports and replacing usage with Postgres (Kysely) in migrated files. Use during Mongo-to-Postgres migration, when migrating routers/actions/utils off #services/mongo/db, or when the user says not to touch mongo/db.ts.
|
||||
---
|
||||
|
||||
# Mongo → Postgres migration — off-limits Mongo layer
|
||||
# Mongo → Postgres migration - off-limits Mongo layer
|
||||
|
||||
## Hard rules
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ description: Prefer `return` over `return await` in async functions when the awa
|
||||
|
||||
In an `async function`, **`return await x` and `return x` are equivalent** for callers when `x` is a Promise (or thenable): both return a Promise with the same fulfillment and rejection.
|
||||
|
||||
Prefer **`return x`** — the extra `await` adds a microtask and obscures that you are simply forwarding the result.
|
||||
Prefer **`return x`** - the extra `await` adds a microtask and obscures that you are simply forwarding the result.
|
||||
|
||||
```typescript
|
||||
// Prefer
|
||||
@@ -17,7 +17,7 @@ async function load() {
|
||||
return fetchData();
|
||||
}
|
||||
|
||||
// Avoid (unless you need await for control flow — see below)
|
||||
// Avoid (unless you need await for control flow - see below)
|
||||
async function load() {
|
||||
return await fetchData();
|
||||
}
|
||||
@@ -40,7 +40,7 @@ async function safeLoad() {
|
||||
}
|
||||
```
|
||||
|
||||
- **`finally`** that must run after the inner work settles (same idea — often needs `await` in the `try`).
|
||||
- **`finally`** that must run after the inner work settles (same idea - often needs `await` in the `try`).
|
||||
|
||||
If there is **no** `try`/`catch`/`finally` depending on that promise settling inside the function, **do not** use `return await`.
|
||||
|
||||
|
||||
@@ -9,15 +9,15 @@ description: Preserves existing code comments (line, block, JSDoc, region marker
|
||||
|
||||
When changing code, **keep existing comments** in place unless one of the exceptions below applies.
|
||||
|
||||
- **Line comments** (`//`), **block comments** (`/* */`), **JSDoc/TSDoc** (`/** */`), and **region/pragma-style markers** the file already uses — preserve them; do not strip them to “clean up” or shorten the diff.
|
||||
- **Line comments** (`//`), **block comments** (`/* */`), **JSDoc/TSDoc** (`/** */`), and **region/pragma-style markers** the file already uses - preserve them; do not strip them to “clean up” or shorten the diff.
|
||||
- If a comment sits next to changed logic, **update the comment only when the behavior it describes changed**. Otherwise leave it untouched.
|
||||
- **Do not** replace a nuanced comment with nothing, or with a shorter generic note, unless the user asked for that.
|
||||
|
||||
## When removal or heavy edits are OK
|
||||
|
||||
- The user **explicitly** asked to remove comments, delete dead documentation, or “dedupe” comments.
|
||||
- The comment is **factually wrong** after the edit and would mislead readers — then fix or remove that comment and prefer a short accurate replacement over silence.
|
||||
- The **entire block of code** the comment referred to is deleted — remove or relocate the comment so it does not orphan misleading text (or move it if it still applies elsewhere).
|
||||
- The comment is **factually wrong** after the edit and would mislead readers - then fix or remove that comment and prefer a short accurate replacement over silence.
|
||||
- The **entire block of code** the comment referred to is deleted - remove or relocate the comment so it does not orphan misleading text (or move it if it still applies elsewhere).
|
||||
|
||||
## What this is not
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"containerEnv": {
|
||||
"MINIO_ENDPOINT": "http://tachi-s3:9000",
|
||||
"TACHI_EMAIL_FROM": "dev@localhost",
|
||||
"TACHI_EMAIL_AUTH_POSTMARK": "false",
|
||||
"TACHI_EMAIL_HOST": "tachi-mailpit",
|
||||
"TACHI_EMAIL_PORT": "1025",
|
||||
"TACHI_EMAIL_SECURE": "false"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
.git
|
||||
.bun
|
||||
|
||||
# Env files — keep secrets out of the build context; runtime env comes from orchestrator / secrets.
|
||||
# Env files - keep secrets out of the build context; runtime env comes from orchestrator / secrets.
|
||||
.env
|
||||
.env.*
|
||||
**/.env
|
||||
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
VITE_RECAPTCHA_KEY: "6LcsYbIpAAAAAEJffjIXmbQcxj_SBZG7BnSPjF4L"
|
||||
VITE_SERVER_URL: "https://v3-boku.tachi.ac"
|
||||
VITE_DISCORD: "https://discord.gg/E4n8ZZF5ES"
|
||||
VITE_CDN_URL: "https://cdn-boku.tachi.ac"
|
||||
VITE_CDN_URL: "https://cdn-v3-boku.tachi.ac"
|
||||
TACHI_NAME: "Bokutachi"
|
||||
BUILD_OUT_DIR: /tmp/boku
|
||||
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
VITE_MIN_CLIENT_ID: "A0DBDBB063CD800530EF01C6488B282137E0191E"
|
||||
VITE_DISCORD: "https://discord.gg/NNgGJbpQUj"
|
||||
VITE_TCHIC_MODE: "kamai"
|
||||
VITE_CDN_URL: "https://cdn-kamai.tachi.ac"
|
||||
VITE_CDN_URL: "https://cdn-v3-kamai.tachi.ac"
|
||||
VITE_GIT_REPO: "GitHub:zkldi/Tachi"
|
||||
VITE_RECAPTCHA_KEY: "6LcsYbIpAAAAAEJffjIXmbQcxj_SBZG7BnSPjF4L"
|
||||
TACHI_NAME: "Kamaitachi"
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
VITE_MANDATE_LOGIN: true
|
||||
VITE_SERVER_URL: "https://dev.tachi.ac"
|
||||
VITE_TCHIC_MODE: "omni"
|
||||
VITE_CDN_URL: "https://cdn-staging.tachi.ac"
|
||||
VITE_CDN_URL: "https://cdn-dev.tachi.ac"
|
||||
VITE_GIT_REPO: "GitHub:zkldi/Tachi"
|
||||
VITE_RECAPTCHA_KEY: "6LcsYbIpAAAAAEJffjIXmbQcxj_SBZG7BnSPjF4L"
|
||||
TACHI_NAME: "Tachi Dev"
|
||||
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
printf '%s' "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
|
||||
|
||||
- name: Deploy updates
|
||||
- name: Deploy seeds
|
||||
env:
|
||||
TACHI_HOST: ${{ secrets.TACHI_HOST }}
|
||||
run: ssh "ci@${TACHI_HOST}" /home/ci/tachi-devops/scripts/deploy_seeds.sh
|
||||
run: ssh -i ~/.ssh/id_rsa "ci@${TACHI_HOST}" /home/ci/tachi-deploy/scripts/deploy-seeds.sh "${{ github.sha }}"
|
||||
|
||||
@@ -121,12 +121,17 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
|
||||
|
||||
- name: Read server package version
|
||||
id: server_version
|
||||
run: |
|
||||
echo "version=$(jq -r .version typescript/server/package.json)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build and push unified server image
|
||||
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile.server
|
||||
target: prod-unified
|
||||
target: prod
|
||||
push: ${{ github.ref == 'refs/heads/main' }}
|
||||
provenance: true
|
||||
sbom: true
|
||||
@@ -135,6 +140,7 @@ jobs:
|
||||
ghcr.io/zkldi/tachi:main
|
||||
ghcr.io/zkldi/tachi:latest
|
||||
build-args: |
|
||||
VERSION=${{ steps.server_version.outputs.version }}
|
||||
COMMIT_HASH=${{ github.sha }}
|
||||
cache-from: type=gha,scope=server-${{ github.ref_name }}
|
||||
cache-to: type=gha,mode=max,scope=server-${{ github.ref_name }}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Postgres backup/restore via Docker (host only). Requires `docker` on PATH — not available
|
||||
# Postgres backup/restore via Docker (host only). Requires `docker` on PATH - not available
|
||||
# inside the devcontainer, so these recipes fail fast there.
|
||||
|
||||
POSTGRES_CONTAINER := "tachi-postgres"
|
||||
|
||||
+4
-2
@@ -13,8 +13,10 @@ check-all-docker-builds: assert-docker-host
|
||||
set -euo pipefail
|
||||
root="$(git rev-parse --show-toplevel)"
|
||||
sha="$(git -C "$root" rev-parse --short HEAD)"
|
||||
echo "==> server (prod-unified)"
|
||||
docker build -f "$root/docker/Dockerfile.server" --target prod-unified \
|
||||
server_ver="$(jq -r .version "$root/typescript/server/package.json")"
|
||||
echo "==> server (prod)"
|
||||
docker build -f "$root/docker/Dockerfile.server" --target prod \
|
||||
--build-arg "VERSION=$server_ver" \
|
||||
--build-arg "COMMIT_HASH=$sha" -t tachi-server:check "$root"
|
||||
echo "==> bot"
|
||||
docker build -f "$root/docker/Dockerfile.bot" -t tachi-bot:check "$root"
|
||||
|
||||
@@ -128,6 +128,9 @@ CREATE TABLE "job_queue" (
|
||||
CREATE INDEX job_queue_partial_index_btree
|
||||
ON job_queue (created_at, status)
|
||||
WHERE status = 0;
|
||||
CREATE INDEX job_queue_dequeue_idx
|
||||
ON job_queue (scheduled_for ASC, created_at ASC)
|
||||
WHERE status = 0;
|
||||
|
||||
CREATE TABLE "cron_task" (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -706,7 +709,6 @@ CREATE TABLE "import_timing" (
|
||||
CREATE TABLE "score" (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
|
||||
-- TODO(zk): Redundant? joins on session(id).
|
||||
user_id BIGINT REFERENCES account(id) NOT NULL,
|
||||
chart_id TEXT REFERENCES chart(id) NOT NULL,
|
||||
|
||||
@@ -915,6 +917,7 @@ CREATE TABLE "pb" (
|
||||
CREATE TABLE "pb_composed_from" (
|
||||
pb_id UUID REFERENCES pb(row_id) NOT NULL,
|
||||
score_id TEXT REFERENCES score(id) NOT NULL,
|
||||
merge_name TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY (pb_id, score_id)
|
||||
);
|
||||
@@ -1047,7 +1050,7 @@ CREATE TABLE "quest" (
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
|
||||
-- Array<QuestSection> — sections with titles, descs, and goal references.
|
||||
-- Array<QuestSection> - sections with titles, descs, and goal references.
|
||||
-- Too structured/variable to flatten usefully.
|
||||
quest_data JSONB NOT NULL
|
||||
);
|
||||
@@ -1131,7 +1134,7 @@ CREATE INDEX score_user_recent_idx ON score (user_id, time_added DESC);
|
||||
CREATE INDEX score_user_highlights_idx ON score (user_id, time_added DESC)
|
||||
WHERE highlight = true;
|
||||
|
||||
-- "All scores on chart X" — chart pages, playcount, import duplicate check.
|
||||
-- "All scores on chart X" - chart pages, playcount, import duplicate check.
|
||||
-- Needed separately because score_user_chart_idx has user_id as the left column.
|
||||
CREATE INDEX score_chart_idx ON score (chart_id);
|
||||
|
||||
@@ -1142,7 +1145,7 @@ CREATE INDEX score_import_uncommitted_idx ON score (import_id)
|
||||
|
||||
CREATE UNIQUE INDEX orphan_score_orphan_id_key ON orphan_score (orphan_id);
|
||||
|
||||
-- pb (5M rows) — hottest table
|
||||
-- pb (5M rows) - hottest table
|
||||
-- The single most important index in the schema.
|
||||
-- Serves: chart_leaderboard view, leaderboard pagination, and the COUNT rank
|
||||
-- queries ("how many PBs beat mine?"). chart_id partitions the 5M rows into
|
||||
@@ -1215,8 +1218,8 @@ CREATE INDEX folder_view_user_idx ON folder_view (user_id, last_viewed DESC);
|
||||
-- so game-first lookups need their own index.
|
||||
CREATE INDEX game_stats_snapshot_game_idx ON game_stats_snapshot (game, timestamp DESC);
|
||||
|
||||
-- chart — general
|
||||
-- "All charts for song X" — song pages, chart listing. Postgres does NOT
|
||||
-- chart - general
|
||||
-- "All charts for song X" - song pages, chart listing. Postgres does NOT
|
||||
-- auto-index FK columns, so this needs to be explicit.
|
||||
CREATE INDEX chart_song_idx ON chart (song_id);
|
||||
|
||||
@@ -1232,7 +1235,7 @@ CREATE UNIQUE INDEX chart_primary_song_difficulty_idx
|
||||
ON chart (game, song_id, difficulty)
|
||||
WHERE is_primary = true;
|
||||
|
||||
-- chart — game-specific JSONB expression indexes
|
||||
-- chart - game-specific JSONB expression indexes
|
||||
-- Partial expression indexes mirroring the per-game Mongo collections.
|
||||
-- Used during score import to look up charts by their game-native ID or hash.
|
||||
|
||||
@@ -1303,4 +1306,20 @@ CREATE TABLE "priv_discord_user_map" (
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX priv_discord_user_map_user_id_idx ON priv_discord_user_map (user_id);
|
||||
-- <== End bot
|
||||
-- <== End bot
|
||||
|
||||
-- Optional read-only role used by Grafana (may not exist in local/dev).
|
||||
DO $grant$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ro') THEN
|
||||
EXECUTE 'GRANT USAGE ON SCHEMA public TO grafana_ro';
|
||||
EXECUTE 'GRANT SELECT ON ALL TABLES IN SCHEMA public TO grafana_ro';
|
||||
EXECUTE 'GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO grafana_ro';
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ro')
|
||||
AND EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'tachi') THEN
|
||||
EXECUTE 'ALTER DEFAULT PRIVILEGES FOR ROLE tachi IN SCHEMA public GRANT SELECT ON TABLES TO grafana_ro';
|
||||
EXECUTE 'ALTER DEFAULT PRIVILEGES FOR ROLE tachi IN SCHEMA public GRANT SELECT ON SEQUENCES TO grafana_ro';
|
||||
END IF;
|
||||
END
|
||||
$grant$;
|
||||
@@ -1,17 +0,0 @@
|
||||
-- Grafana local-dev role (see dev/postgres-init.sql): init runs before migrations, so
|
||||
-- GRANT SELECT ON ALL TABLES saw no tables; default privileges can also miss edge cases.
|
||||
-- Re-apply read access to all existing objects; no-op when grafana_ro is absent.
|
||||
DO $grant$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ro') THEN
|
||||
EXECUTE 'GRANT USAGE ON SCHEMA public TO grafana_ro';
|
||||
EXECUTE 'GRANT SELECT ON ALL TABLES IN SCHEMA public TO grafana_ro';
|
||||
EXECUTE 'GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO grafana_ro';
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ro')
|
||||
AND EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'tachi') THEN
|
||||
EXECUTE 'ALTER DEFAULT PRIVILEGES FOR ROLE tachi IN SCHEMA public GRANT SELECT ON TABLES TO grafana_ro';
|
||||
EXECUTE 'ALTER DEFAULT PRIVILEGES FOR ROLE tachi IN SCHEMA public GRANT SELECT ON SEQUENCES TO grafana_ro';
|
||||
END IF;
|
||||
END
|
||||
$grant$;
|
||||
@@ -1,4 +0,0 @@
|
||||
-- Fast dequeue of queued (status = 0) jobs ordered by when they are due.
|
||||
CREATE INDEX job_queue_dequeue_idx
|
||||
ON job_queue (scheduled_for ASC, created_at ASC)
|
||||
WHERE status = 0;
|
||||
+1
-1
@@ -39,7 +39,7 @@ function seedMinioCdn {
|
||||
# Debian package `minio-client` installs the `minio-client` binary (same CLI as upstream `mc`).
|
||||
# override endpoint with MINIO_ENDPOINT (devcontainer sets http://tachi-s3:9000).
|
||||
if ! command -v minio-client &> /dev/null; then
|
||||
echo "Couldn't find MinIO client (minio-client). On Debian: apt install minio-client — see Dockerfile.dev."
|
||||
echo "Couldn't find MinIO client (minio-client). On Debian: apt install minio-client - see Dockerfile.dev."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Fisher
|
||||
|
||||
> A plugin manager for [Fish](https://fishshell.com)—your friendly interactive shell. [Snag fresh plugins!](https://github.com/jorgebucaran/awsm.fish#readme)
|
||||
> A plugin manager for [Fish](https://fishshell.com)-your friendly interactive shell. [Snag fresh plugins!](https://github.com/jorgebucaran/awsm.fish#readme)
|
||||
|
||||
Take control of functions, completions, bindings, and snippets from the command line. Unleash your shell's true potential, perfect your prompt, and craft repeatable configurations across different systems effortlessly. Fisher's zero impact on shell startup keeps your shell zippy and responsive. No gimmicks, just smooth sailing!
|
||||
|
||||
- Fisher is 100% pure-Fish, making it easy to contribute or modify
|
||||
- Scorching fast concurrent plugin downloads that'll make you question reality
|
||||
- Zero configuration needed—we're not kidding!
|
||||
- Zero configuration needed-we're not kidding!
|
||||
- Oh My Fish! plugins supported too
|
||||
|
||||
> #### ☝️ [Upgrading from Fisher `3.x` or older? Strap in and read this!](https://github.com/jorgebucaran/fisher/issues/652)
|
||||
@@ -29,7 +29,7 @@ To install plugins, use the `install` command and point it to the GitHub reposit
|
||||
fisher install jorgebucaran/nvm.fish
|
||||
```
|
||||
|
||||
> Wanna install from GitLab? No problemo—just prepend `gitlab.com/` to the plugin path.
|
||||
> Wanna install from GitLab? No problemo-just prepend `gitlab.com/` to the plugin path.
|
||||
|
||||
You can also snag a specific version of a plugin by adding an `@` symbol after the plugin name, followed by a tag, branch, or [commit](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefcommit-ishacommit-ishalsocommittish).
|
||||
|
||||
|
||||
+21
-55
@@ -1,74 +1,39 @@
|
||||
# Build context must be the repo root (same as GHA docker-push: context `.`).
|
||||
# Respects .dockerignore (see repo root). Full tree copy keeps workspace installs correct.
|
||||
# docker build -f docker/Dockerfile.server --target prod-unified --build-arg COMMIT_HASH="$(git rev-parse --short HEAD)" -t tachi-server:local .
|
||||
# docker build -f docker/Dockerfile.server --target prod-api .
|
||||
# docker build -f docker/Dockerfile.server --target prod-job-worker .
|
||||
# docker build -f docker/Dockerfile.server --target prod-cron-worker .
|
||||
# docker build -f docker/Dockerfile.server \
|
||||
# --build-arg VERSION="$(jq -r .version typescript/server/package.json)" \
|
||||
# --build-arg COMMIT_HASH="$(git rev-parse --short HEAD)" -t tachi-server:local .
|
||||
#
|
||||
# Default CMD runs the API server. Override to run a worker:
|
||||
# docker run tachi-server:local bun run ./typescript/server/src/job-queue-worker.ts
|
||||
# docker run tachi-server:local bun run ./typescript/server/src/cron-worker.ts
|
||||
#
|
||||
# Worker containers use a file-based heartbeat (/tmp/worker-heartbeat) rather than HTTP,
|
||||
# so their HEALTHCHECK must be configured at the deployment layer (compose/k8s), not here.
|
||||
#
|
||||
# NOTE: bun build --compile is NOT used here because it's
|
||||
# FUCKING BROKEN
|
||||
|
||||
FROM oven/bun:alpine AS base
|
||||
FROM oven/bun:alpine AS deps
|
||||
WORKDIR /app
|
||||
|
||||
# --- install: full tree so bun sees every workspace package ---
|
||||
FROM base AS deps
|
||||
|
||||
COPY . .
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# --- API image ---
|
||||
FROM oven/bun:alpine AS prod-api
|
||||
|
||||
ARG COMMIT_HASH
|
||||
ENV COMMIT_HASH=${COMMIT_HASH}
|
||||
ENV MIGRATIONS_DIR=/app/db/migrations
|
||||
ENV PORT=8080
|
||||
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app .
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s \
|
||||
CMD curl -sf http://localhost:8080/.deploy/up || exit 1
|
||||
|
||||
CMD ["bun", "run", "./typescript/server/src/main.ts"]
|
||||
|
||||
# --- Job queue worker (no HTTP port) ---
|
||||
FROM oven/bun:alpine AS prod-job-worker
|
||||
|
||||
ARG COMMIT_HASH
|
||||
ENV COMMIT_HASH=${COMMIT_HASH}
|
||||
ENV MIGRATIONS_DIR=/app/db/migrations
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app .
|
||||
|
||||
CMD ["bun", "run", "./typescript/server/src/job-queue-worker.ts"]
|
||||
|
||||
# --- Cron worker (no HTTP port) ---
|
||||
FROM oven/bun:alpine AS prod-cron-worker
|
||||
|
||||
ARG COMMIT_HASH
|
||||
ENV COMMIT_HASH=${COMMIT_HASH}
|
||||
ENV MIGRATIONS_DIR=/app/db/migrations
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app .
|
||||
|
||||
CMD ["bun", "run", "./typescript/server/src/cron-worker.ts"]
|
||||
|
||||
# --- All-in-one image.
|
||||
FROM oven/bun:alpine AS prod-unified
|
||||
FROM oven/bun:alpine AS prod
|
||||
|
||||
ARG VERSION
|
||||
ARG COMMIT_HASH
|
||||
# Fail the build if either arg is omitted (Docker has no required-ARG syntax).
|
||||
RUN set -eu; \
|
||||
: "${VERSION:?Dockerfile.server: VERSION build-arg is required}"; \
|
||||
: "${COMMIT_HASH:?Dockerfile.server: COMMIT_HASH build-arg is required}";
|
||||
|
||||
ENV VERSION=${VERSION}
|
||||
ENV COMMIT_HASH=${COMMIT_HASH}
|
||||
ENV MIGRATIONS_DIR=/app/db/migrations
|
||||
ENV PORT=8080
|
||||
|
||||
# curl is only needed by the API healthcheck below; workers don't use it.
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
WORKDIR /app
|
||||
@@ -76,6 +41,7 @@ COPY --from=deps /app .
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
# Matches the default CMD (API server). Worker deployments must override this.
|
||||
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s \
|
||||
CMD curl -sf http://localhost:8080/.deploy/up || exit 1
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ Empty Object.
|
||||
|
||||
`POST /api/v1/admin/recalc-pbs`
|
||||
|
||||
Inserts every distinct `(user_id, chart_id)` from the **`score`** table into **`pb_dirty`**, then **drains** `pb_dirty` and downstream **`session_dirty`** / **`game_profile_dirty`** queues until nothing remains (same batching as the background worker, but the HTTP request waits until idle). Intended when PBs may be out of sync (e.g. after a bad migration). There is **no request body** and no filter—always all distinct pairs that appear on scores.
|
||||
Inserts every distinct `(user_id, chart_id)` from the **`score`** table into **`pb_dirty`**, then **drains** `pb_dirty` and downstream **`session_dirty`** / **`game_profile_dirty`** queues until nothing remains (same batching as the background worker, but the HTTP request waits until idle). Intended when PBs may be out of sync (e.g. after a bad migration). There is **no request body** and no filter-always all distinct pairs that appear on scores.
|
||||
|
||||
### Permissions
|
||||
|
||||
@@ -191,7 +191,7 @@ Empty Object.
|
||||
|
||||
`POST /api/v1/admin/recalc`
|
||||
|
||||
Enqueues **every chart** into **`score_rederive`**, then **drains** `score_rederive` and downstream **`pb_dirty`**, **`session_dirty`**, and **`game_profile_dirty`** queues until nothing remains (the request waits until idle). Re-runs `scoreDeriver` and `scoreCalcs` for every score. There is **no request body** and no filter—always all charts.
|
||||
Enqueues **every chart** into **`score_rederive`**, then **drains** `score_rederive` and downstream **`pb_dirty`**, **`session_dirty`**, and **`game_profile_dirty`** queues until nothing remains (the request waits until idle). Re-runs `scoreDeriver` and `scoreCalcs` for every score. There is **no request body** and no filter-always all charts.
|
||||
|
||||
### Permissions
|
||||
|
||||
|
||||
@@ -236,13 +236,13 @@ provide callback URLs inside emails.
|
||||
|
||||
SMTP is always configured. Set:
|
||||
|
||||
- `TACHI_EMAIL_FROM` — `From` header (must match a verified sender when using Postmark).
|
||||
- `TACHI_EMAIL_AUTH_POSTMARK` — `true` or `false`. When `true`, uses Postmark’s SMTP
|
||||
endpoint; set `TACHI_EMAIL_AUTH_USER` and `TACHI_EMAIL_AUTH_PASS` to your server API token
|
||||
(both are the token for Postmark SMTP).
|
||||
- When Postmark is `false`: `TACHI_EMAIL_HOST`, `TACHI_EMAIL_PORT`, `TACHI_EMAIL_SECURE`
|
||||
(`true` / `false`), and optionally `TACHI_EMAIL_AUTH_USER` / `TACHI_EMAIL_AUTH_PASS`
|
||||
for servers that require auth (local Mailpit typically needs no auth).
|
||||
- `TACHI_EMAIL_FROM` - `From` header (must match a verified sender when using Postmark).
|
||||
- `TACHI_EMAIL_HOST`, `TACHI_EMAIL_PORT`, `TACHI_EMAIL_SECURE` (`true` / `false`).
|
||||
- Optionally `TACHI_EMAIL_AUTH_USER` / `TACHI_EMAIL_AUTH_PASS` for SMTP auth (local Mailpit
|
||||
typically needs none).
|
||||
- For Postmark, use host `smtp.postmarkapp.com` (usually port `587` with `TACHI_EMAIL_SECURE=false`)
|
||||
and set either auth field to your server API token (both username and password are the token for
|
||||
Postmark SMTP).
|
||||
|
||||
`TRANSPORT_OPS` is derived from these variables and passed to Nodemailer.
|
||||
|
||||
@@ -272,6 +272,18 @@ interface INVITE_CODE_CONFIG: {
|
||||
};
|
||||
```
|
||||
|
||||
### TACHI_INVITE_ADMIN_INITIAL_INVITE_CODE
|
||||
|
||||
- Type: String (Optional, environment variable only)
|
||||
|
||||
A one-time bootstrap invite code for first-time instance setup. When `INVITE_CODE_CONFIG` is
|
||||
configured, new instances have a chicken-and-egg problem: registration requires an invite code,
|
||||
but invite codes can only be created by an existing user.
|
||||
|
||||
Set this environment variable to a long, random secret. The first person to register with this
|
||||
code - while the `account` table is still empty - becomes the site admin. Once the first admin
|
||||
exists, the code is no longer accepted.
|
||||
|
||||
### TACHI_CONFIG
|
||||
|
||||
- Type: TACHI_CONFIG
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Tachi API — endpoint latencies, score import durations, error rates & system health",
|
||||
"description": "Tachi API - endpoint latencies, score import durations, error rates & system health",
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 1,
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
"resolveJsonModule": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
// all packages run via bun directly from source — no emit needed
|
||||
// all packages run via bun directly from source - no emit needed
|
||||
"noEmit": true,
|
||||
"types": []
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export default defineConfig({
|
||||
DISCORD_SERVER_ID: "test-server-id",
|
||||
},
|
||||
|
||||
// Parallel test execution — each worker gets its own isolated Postgres database.
|
||||
// Parallel test execution - each worker gets its own isolated Postgres database.
|
||||
fileParallelism: true,
|
||||
globalSetup: "./vitest.globalSetup.ts",
|
||||
setupFiles: "./vitest.setup.ts",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
/**
|
||||
* Global vitest setup — runs ONCE before any workers start.
|
||||
* Global vitest setup - runs ONCE before any workers start.
|
||||
*
|
||||
* Creates a fully-migrated template database. Workers clone from it
|
||||
* instead of running migrations themselves, which is much faster.
|
||||
|
||||
@@ -20,7 +20,7 @@ const POSTGRES_HOST = "tachi-postgres";
|
||||
const POSTGRES_USER = "tachi";
|
||||
const POSTGRES_PASS = "tachi";
|
||||
|
||||
// Set POSTGRES_URL before any app code is imported — config.ts reads it at load time.
|
||||
// Set POSTGRES_URL before any app code is imported - config.ts reads it at load time.
|
||||
process.env.POSTGRES_URL = `postgresql://${POSTGRES_USER}:${POSTGRES_PASS}@${POSTGRES_HOST}/${WORKER_DB_NAME}`;
|
||||
|
||||
// Mock global fetch before any app code is imported.
|
||||
@@ -45,7 +45,7 @@ globalThis.fetch = (url: string | Request | URL): Promise<Response> => {
|
||||
} as unknown as Response);
|
||||
}
|
||||
|
||||
// Blow up loudly if any unexpected fetch slips through — this keeps tests hermetic.
|
||||
// Blow up loudly if any unexpected fetch slips through - this keeps tests hermetic.
|
||||
throw new Error(`Unexpected fetch() in test: ${url.toString()}`);
|
||||
};
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ type ActionsResponse = {
|
||||
function durationMs(start: string, end: string): string {
|
||||
const ms = Date.parse(end) - Date.parse(start);
|
||||
if (Number.isNaN(ms)) {
|
||||
return "—";
|
||||
return "-";
|
||||
}
|
||||
if (ms < 1000) {
|
||||
return `${ms}ms`;
|
||||
@@ -196,11 +196,11 @@ export default function AdminActionsPage() {
|
||||
{action.username}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-body-secondary">—</span>
|
||||
<span className="text-body-secondary">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="font-monospace small">
|
||||
{action.ip ?? "—"}
|
||||
{action.ip ?? "-"}
|
||||
</td>
|
||||
<td className="align-middle">
|
||||
<Button
|
||||
|
||||
@@ -31,11 +31,11 @@ type CronResponse = {
|
||||
|
||||
function durationMs(startedAt: string, completedAt: string | null): string {
|
||||
if (!completedAt) {
|
||||
return "—";
|
||||
return "-";
|
||||
}
|
||||
const ms = Date.parse(completedAt) - Date.parse(startedAt);
|
||||
if (Number.isNaN(ms)) {
|
||||
return "—";
|
||||
return "-";
|
||||
}
|
||||
if (ms < 1000) {
|
||||
return `${ms}ms`;
|
||||
@@ -175,7 +175,7 @@ export default function AdminCronJobsPage() {
|
||||
</details>
|
||||
) : (
|
||||
<span className="text-body-secondary">
|
||||
—
|
||||
-
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function AdminOperationsPage() {
|
||||
}}
|
||||
value={announcementGame === "" ? "" : announcementGame}
|
||||
>
|
||||
<option value="">— Site-wide —</option>
|
||||
<option value="">- Site-wide -</option>
|
||||
{TachiConfig.GAME_GROUPS.map((g) => (
|
||||
<option key={g} value={g}>
|
||||
{g}
|
||||
@@ -62,7 +62,7 @@ export default function AdminOperationsPage() {
|
||||
onChange={(e) => setAnnouncementPlaytype(e.target.value)}
|
||||
value={announcementPlaytype}
|
||||
>
|
||||
<option value="">—</option>
|
||||
<option value="">-</option>
|
||||
{announcementGameConfig.playtypes.map((pt) => (
|
||||
<option key={pt} value={pt}>
|
||||
{FormatGame(
|
||||
|
||||
@@ -187,7 +187,7 @@ export default function UserOrphansPage({ reqUser }: { reqUser: UserDocument })
|
||||
|
||||
<Modal onHide={() => setDetailOpen(false)} show={detailOpen} size="lg">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Orphan details — {detailTitle}</Modal.Title>
|
||||
<Modal.Title>Orphan details - {detailTitle}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{detailLoading ? (
|
||||
@@ -221,7 +221,7 @@ export default function UserOrphansPage({ reqUser }: { reqUser: UserDocument })
|
||||
]}
|
||||
rowFunction={(o) => (
|
||||
<tr key={o.orphanID}>
|
||||
<td>{o.summary ?? "—"}</td>
|
||||
<td>{o.summary ?? "-"}</td>
|
||||
<td>
|
||||
<code className="small">{o.importType}</code>
|
||||
</td>
|
||||
@@ -230,7 +230,7 @@ export default function UserOrphansPage({ reqUser }: { reqUser: UserDocument })
|
||||
o.gameGroup}
|
||||
</td>
|
||||
<td>{new Date(o.timeInserted).toLocaleString()}</td>
|
||||
<td className="small text-muted">{o.message ?? "—"}</td>
|
||||
<td className="small text-muted">{o.message ?? "-"}</td>
|
||||
<td>
|
||||
<div className="d-flex flex-wrap gap-1">
|
||||
<Button
|
||||
|
||||
+58
-2
@@ -6,18 +6,19 @@ import DebugContent from "#components/util/DebugContent";
|
||||
import Divider from "#components/util/Divider";
|
||||
import EditableText from "#components/util/EditableText";
|
||||
import Icon from "#components/util/Icon";
|
||||
import LinkButton from "#components/util/LinkButton";
|
||||
import Loading from "#components/util/Loading";
|
||||
import useApiQuery from "#components/util/query/useApiQuery";
|
||||
import { UserContext } from "#context/UserContext";
|
||||
import { UserSettingsContext } from "#context/UserSettingsContext";
|
||||
import { type SessionReturns } from "#types/api-returns";
|
||||
import { type SessionAdjacentReturns, type SessionReturns } from "#types/api-returns";
|
||||
import { type UGPT } from "#types/react";
|
||||
import { APIFetchV1 } from "#util/api";
|
||||
import { CreateChartMap, CreateScoreIDMap, CreateSongMap } from "#util/data";
|
||||
import React, { useContext, useMemo, useState } from "react";
|
||||
import { Badge, Button, Col, Row } from "react-bootstrap";
|
||||
import { Redirect, useParams } from "react-router-dom";
|
||||
import { GameToGameGroup, GetGameGroupConfig } from "tachi-common";
|
||||
import { GameToGameGroup, GetGameGroupConfig, type SessionDocument } from "tachi-common";
|
||||
|
||||
export default function SpecificSessionPage({ reqUser, game }: UGPT) {
|
||||
const { sessionID } = useParams<{ sessionID: string }>();
|
||||
@@ -127,8 +128,20 @@ function SessionPage({ data, game }: { data: SessionReturns } & UGPT) {
|
||||
);
|
||||
};
|
||||
|
||||
const { data: adjacentData } = useApiQuery<SessionAdjacentReturns>(
|
||||
`/sessions/${session.sessionID}/adjacent`,
|
||||
);
|
||||
|
||||
return (
|
||||
<Row className="justify-content-center">
|
||||
<Col xs={12}>
|
||||
<SessionNavRow
|
||||
adjacent={adjacentData ?? null}
|
||||
currentName={session.name}
|
||||
game={game}
|
||||
username={user.username}
|
||||
/>
|
||||
</Col>
|
||||
<Col className="text-center" xs={12}>
|
||||
<div className="d-flex flex-column gap-2 flex-wrap justify-content-center align-items-center">
|
||||
<EditableText
|
||||
@@ -259,3 +272,46 @@ function SessionPage({ data, game }: { data: SessionReturns } & UGPT) {
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
function SessionNavRow({
|
||||
adjacent,
|
||||
currentName,
|
||||
game,
|
||||
username,
|
||||
}: {
|
||||
adjacent: SessionAdjacentReturns | null;
|
||||
currentName: string;
|
||||
game: string;
|
||||
username: string;
|
||||
}) {
|
||||
const sessionUrl = (s: SessionDocument) =>
|
||||
`/u/${username}/games/${s.game}/sessions/${s.sessionID}`;
|
||||
|
||||
return (
|
||||
<div className="d-flex align-items-center justify-content-between mb-4">
|
||||
<div style={{ minWidth: "140px" }}>
|
||||
{adjacent?.prev ? (
|
||||
<LinkButton to={sessionUrl(adjacent.prev)} variant="outline-secondary">
|
||||
« Prev
|
||||
</LinkButton>
|
||||
) : (
|
||||
<Button disabled variant="outline-secondary">
|
||||
« Prev
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-muted text-truncate px-2">{currentName}</span>
|
||||
<div style={{ minWidth: "140px" }} className="text-end">
|
||||
{adjacent?.next ? (
|
||||
<LinkButton to={sessionUrl(adjacent.next)} variant="outline-secondary">
|
||||
Next »
|
||||
</LinkButton>
|
||||
) : (
|
||||
<Button disabled variant="outline-secondary">
|
||||
Next »
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,10 @@ import {
|
||||
type UserDocument,
|
||||
} from "tachi-common";
|
||||
|
||||
function isActivityInteractiveTarget(el: HTMLElement) {
|
||||
return Boolean(el.closest("a, button, input, textarea, select, [role='button']"));
|
||||
}
|
||||
|
||||
// Records activity for a group of users on a GPT. Also used for single users.
|
||||
export default function Activity({
|
||||
url,
|
||||
@@ -291,7 +295,16 @@ function ScoresActivity({
|
||||
<div className="timeline-item timeline-hover my-4">
|
||||
<div className="timeline-badge bg-warning"></div>
|
||||
<div className="timeline-content flex-nowrap">
|
||||
<div className="timeline-content-inner" onClick={() => setShow(!show)}>
|
||||
<div
|
||||
className="timeline-content-inner activity-entry-toggle"
|
||||
onClick={(e) => {
|
||||
if (isActivityInteractiveTarget(e.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setShow(!show);
|
||||
}}
|
||||
>
|
||||
<div className="timeline-content-title">
|
||||
<span className="me-2">
|
||||
<ProfilePicture size="sm" toGPT={{ game }} user={user} />
|
||||
@@ -323,10 +336,10 @@ function ScoresActivity({
|
||||
</div>
|
||||
|
||||
{show && (
|
||||
<>
|
||||
<div className="activity-expand-body">
|
||||
<Divider />
|
||||
<ScoreTable dataset={dataset} game={game} noTopDisplayStr />
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -367,7 +380,16 @@ function GoalActivity({
|
||||
<div className="timeline-item timeline-hover my-4">
|
||||
<div className="timeline-badge bg-warning"></div>
|
||||
<div className="timeline-content">
|
||||
<div className="timeline-content-inner" onClick={() => setShow(!show)}>
|
||||
<div
|
||||
className="timeline-content-inner activity-entry-toggle"
|
||||
onClick={(e) => {
|
||||
if (isActivityInteractiveTarget(e.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setShow(!show);
|
||||
}}
|
||||
>
|
||||
<div className="timeline-content-title">
|
||||
<span className="me-2">
|
||||
<ProfilePicture size="sm" toGPT={{ game }} user={user} />
|
||||
@@ -399,7 +421,7 @@ function GoalActivity({
|
||||
</div>
|
||||
|
||||
{show && (
|
||||
<>
|
||||
<div className="activity-expand-body">
|
||||
<Divider />
|
||||
<div className="ps-4">
|
||||
{data.goals.map((e) => (
|
||||
@@ -410,7 +432,7 @@ function GoalActivity({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -485,7 +507,16 @@ function SessionActivity({
|
||||
<div className="timeline-item timeline-hover">
|
||||
<div className={`timeline-badge bg-${data.highlight ? "warning" : "secondary"}`}></div>
|
||||
<div className="timeline-content">
|
||||
<div className="timeline-content-inner" onClick={() => setShow(!show)}>
|
||||
<div
|
||||
className="timeline-content-inner activity-entry-toggle"
|
||||
onClick={(e) => {
|
||||
if (isActivityInteractiveTarget(e.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setShow(!show);
|
||||
}}
|
||||
>
|
||||
<div className="timeline-content-title">
|
||||
<span className="me-2">
|
||||
<ProfilePicture size="sm" toGPT={{ game }} user={user} />
|
||||
@@ -529,7 +560,11 @@ function SessionActivity({
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{show && <SessionShower sessionID={data.sessionID} />}
|
||||
{show && (
|
||||
<div className="activity-expand-body">
|
||||
<SessionShower sessionID={data.sessionID} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -131,7 +131,7 @@ export default function ImportInfo({
|
||||
<br />
|
||||
<strong>SongOrChartNotFound</strong> means the score was still{" "}
|
||||
<strong>saved as an orphan</strong> for nightly matching (around 00:01
|
||||
UTC) or manual reprocess — see{" "}
|
||||
UTC) or manual reprocess - see{" "}
|
||||
{user ? (
|
||||
<Link to={`/u/${user.username}/orphans`}>Orphan scores</Link>
|
||||
) : (
|
||||
|
||||
@@ -22,10 +22,10 @@ try {
|
||||
// Do NOT use document.open() / document.write() / document.close() here.
|
||||
// document.open() fires unload/beforeunload/pagehide events, which Vite's HMR
|
||||
// client intercepts to close its WebSocket. The HMR reconnection logic then
|
||||
// triggers a full page reload, re-running this module, failing again — an
|
||||
// triggers a full page reload, re-running this module, failing again - an
|
||||
// infinite refresh loop.
|
||||
//
|
||||
// Do NOT throw either — an uncaught module-evaluation error is another signal
|
||||
// Do NOT throw either - an uncaught module-evaluation error is another signal
|
||||
// that Vite HMR uses to trigger a reload.
|
||||
//
|
||||
// Instead, use direct DOM mutation (no unload events) and console.error.
|
||||
|
||||
@@ -120,7 +120,6 @@ const IIDX_PROFILE_RATING_VALUE_CELL_STYLE: NonNullable<
|
||||
GPTClientImplementation<GamesForGroup["iidx"]>["profileRatingAlgRowStyle"]
|
||||
> = {
|
||||
ktLampRating: { backgroundColor: ChangeOpacity(COLOUR_SET.purple, 0.12) },
|
||||
ktLampRatingNC: { backgroundColor: ChangeOpacity(COLOUR_SET.blue, 0.14) },
|
||||
ktLampRatingHC: { backgroundColor: ChangeOpacity(COLOUR_SET.orange, 0.14) },
|
||||
ktLampRatingEXHC: { backgroundColor: ChangeOpacity(COLOUR_SET.gold, 0.12) },
|
||||
BPI: { backgroundColor: ChangeOpacity(COLOUR_SET.paleBlue, 0.12) },
|
||||
|
||||
@@ -93,3 +93,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Activity feed (Activity.tsx): row hover covers full `.timeline-item`, but clickable
|
||||
// region was only `.timeline-content-inner` inset inside padded `.timeline-content`.
|
||||
// Move padding onto the toggle row so clicks match the highlight.
|
||||
.timeline.activity-timeline {
|
||||
.timeline-item .timeline-content:has(> .activity-entry-toggle:first-child) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.timeline-item .activity-entry-toggle {
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.timeline-item .activity-expand-body {
|
||||
padding: 0 1rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,11 @@ export interface SessionReturns<GPT extends V3Game = V3Game> {
|
||||
user: UserDocument;
|
||||
}
|
||||
|
||||
export interface SessionAdjacentReturns {
|
||||
prev: SessionDocument | null;
|
||||
next: SessionDocument | null;
|
||||
}
|
||||
|
||||
export interface UGPTChartPBComposition<GPT extends V3Game = V3Game> {
|
||||
scores: ScoreDocument<GPT>[];
|
||||
chart: ChartDocument<GPT>;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
files: "src/**/*.test.ts"
|
||||
test-env: NODE_ENV=test
|
||||
timeout: 120
|
||||
ts: true
|
||||
|
||||
# all this is broken now for some reason. who knows.
|
||||
check-coverage: false
|
||||
|
||||
# branches: 0 # 80
|
||||
# functions: 0 # 80
|
||||
# lines: 0 # 80
|
||||
# statements 0: # 80
|
||||
reporter: terse
|
||||
@@ -172,10 +172,6 @@ export const GAME_IIDX_SP_CONF = {
|
||||
description:
|
||||
"A rating system that values your clear lamps on charts. Tierlist information is taken into account.",
|
||||
},
|
||||
ktLampRatingNC: {
|
||||
description:
|
||||
"Your normal clear performance. Tierlist information is taken into account.",
|
||||
},
|
||||
ktLampRatingHC: {
|
||||
description: "Your hard clear performance. Tierlist information is taken into account.",
|
||||
},
|
||||
@@ -195,30 +191,24 @@ export const GAME_IIDX_SP_CONF = {
|
||||
associatedScoreAlgs: ["ktLampRating"],
|
||||
displayOrder: 0,
|
||||
},
|
||||
ktLampRatingNC: {
|
||||
description: `An average of your best 20 ktLampRatingNCs.`,
|
||||
associatedScoreAlgs: ["ktLampRatingNC"],
|
||||
displayOrder: 1,
|
||||
},
|
||||
ktLampRatingHC: {
|
||||
description: `An average of your best 20 ktLampRatingHCs.`,
|
||||
associatedScoreAlgs: ["ktLampRatingHC"],
|
||||
displayOrder: 2,
|
||||
displayOrder: 1,
|
||||
},
|
||||
ktLampRatingEXHC: {
|
||||
description: `An average of your best 20 ktLampRatingEXHCs.`,
|
||||
associatedScoreAlgs: ["ktLampRatingEXHC"],
|
||||
displayOrder: 3,
|
||||
displayOrder: 2,
|
||||
},
|
||||
BPI: {
|
||||
description: `An average of your best 20 BPIs.`,
|
||||
associatedScoreAlgs: ["BPI"],
|
||||
displayOrder: 4,
|
||||
displayOrder: 3,
|
||||
},
|
||||
},
|
||||
sessionRatingAlgs: {
|
||||
ktLampRating: { description: `An average of the best 10 ktLampRatings this session.` },
|
||||
ktLampRatingNC: { description: `An average of the best 10 ktLampRatingNCs this session.` },
|
||||
ktLampRatingHC: { description: `An average of the best 10 ktLampRatingHCs this session.` },
|
||||
ktLampRatingEXHC: {
|
||||
description: `An average of the best 10 ktLampRatingEXHCs this session.`,
|
||||
|
||||
@@ -351,7 +351,7 @@ function encodeBmsTableKey(game: string, key: string): string {
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`No BMS_TABLES row for game=${JSON.stringify(game)} prefix=${JSON.stringify(key)} — add a BMSTableInfo entry or use an ascii-only prefix.`,
|
||||
`No BMS_TABLES row for game=${JSON.stringify(game)} prefix=${JSON.stringify(key)} - add a BMSTableInfo entry or use an ascii-only prefix.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export default defineConfig({
|
||||
NODE_ENV: "test",
|
||||
},
|
||||
exclude: [
|
||||
// Compiled output — not test sources.
|
||||
// Compiled output - not test sources.
|
||||
"build/**",
|
||||
],
|
||||
coverage: {
|
||||
|
||||
@@ -119,7 +119,7 @@ function printHuman(report: CoverageReport): void {
|
||||
|
||||
for (const p of report.packages) {
|
||||
if (!p.present || !p.metrics) {
|
||||
console.log(`${p.id.padEnd(10)} (no data — run: just test-typescript)`);
|
||||
console.log(`${p.id.padEnd(10)} (no data - run: just test-typescript)`);
|
||||
continue;
|
||||
}
|
||||
console.log(row(p.id, p.metrics));
|
||||
|
||||
@@ -254,7 +254,7 @@ migrateCmd
|
||||
for (const info of infos) {
|
||||
const status =
|
||||
info.success === null ? "Pending" : info.success ? "Applied" : "Failed";
|
||||
const appliedAt = info.appliedAt ? formatDate(info.appliedAt) : "—";
|
||||
const appliedAt = info.appliedAt ? formatDate(info.appliedAt) : "-";
|
||||
const desc =
|
||||
info.description.length > descWidth - 2
|
||||
? `${info.description.slice(0, descWidth - 5)}...`
|
||||
|
||||
@@ -142,7 +142,7 @@ async function withAdvisoryLock<T>(client: Client, fn: () => Promise<T>): Promis
|
||||
|
||||
/**
|
||||
* Apply all pending migrations from migrationsDir against the given connection.
|
||||
* Safe to call on every startup — already-applied migrations are skipped.
|
||||
* Safe to call on every startup - already-applied migrations are skipped.
|
||||
* Throws if a previously-applied migration's file has been modified (checksum mismatch).
|
||||
*/
|
||||
export async function applyMigrations(
|
||||
@@ -180,7 +180,7 @@ export async function applyMigrations(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Previously attempted and failed — clear the record and retry.
|
||||
// Previously attempted and failed - clear the record and retry.
|
||||
await client.query(
|
||||
`DELETE FROM "_migration" WHERE version = $1 AND success = false`,
|
||||
[migration.version],
|
||||
@@ -188,7 +188,7 @@ export async function applyMigrations(
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[migrate] Applying ${migration.version} — ${migration.description}...`,
|
||||
`[migrate] Applying ${migration.version} - ${migration.description}...`,
|
||||
);
|
||||
|
||||
const start = process.hrtime.bigint();
|
||||
@@ -334,7 +334,7 @@ export async function revertLastMigration(
|
||||
|
||||
const downSql = readFileSync(downPath, "utf8");
|
||||
|
||||
console.log(`[migrate] Reverting ${latestVersion} — ${latest.description}...`);
|
||||
console.log(`[migrate] Reverting ${latestVersion} - ${latest.description}...`);
|
||||
|
||||
const start = process.hrtime.bigint();
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ export default interface PbComposedFromTable {
|
||||
pb_id: ColumnType<pb_row_id, pb_row_id, pb_row_id>;
|
||||
|
||||
score_id: ColumnType<score_id, score_id, score_id>;
|
||||
|
||||
merge_name: ColumnType<string, string, string>;
|
||||
}
|
||||
|
||||
export type PbComposedFrom = Selectable<PbComposedFromTable>;
|
||||
|
||||
@@ -26,7 +26,7 @@ const base = tseslint.config(
|
||||
...tseslint.configs.recommended,
|
||||
pluginPerfectionist.configs["recommended-natural"],
|
||||
{
|
||||
name: "zenith/base",
|
||||
name: "tachi/base",
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
plugins: {
|
||||
import: pluginImport,
|
||||
@@ -141,10 +141,17 @@ const base = tseslint.config(
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tachi/test-files",
|
||||
files: ["**/*.test.ts"],
|
||||
rules: {
|
||||
"no-await-in-loop": "off",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const node = /** @type {import("@typescript-eslint/utils/ts-eslint").FlatConfig.Config} */ ({
|
||||
name: "zenith/node",
|
||||
name: "tachi/node",
|
||||
files: ["**/*.{js,mjs,cjs,ts,tsx}"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
@@ -154,7 +161,7 @@ const node = /** @type {import("@typescript-eslint/utils/ts-eslint").FlatConfig.
|
||||
});
|
||||
|
||||
const react = /** @type {import("@typescript-eslint/utils/ts-eslint").FlatConfig.Config} */ ({
|
||||
name: "zenith/react",
|
||||
name: "tachi/react",
|
||||
files: ["**/*.tsx"],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
@@ -211,8 +218,10 @@ const react = /** @type {import("@typescript-eslint/utils/ts-eslint").FlatConfig
|
||||
},
|
||||
});
|
||||
|
||||
// unused (from zenith), we don't use remix. We don't even use remix in zenith either
|
||||
// because it got renamed to rrv7.
|
||||
const reactRemix = /** @type {import("@typescript-eslint/utils/ts-eslint").FlatConfig.Config} */ ({
|
||||
name: "zenith/react-remix",
|
||||
name: "tachi/react-remix",
|
||||
languageOptions: {
|
||||
// @ts-expect-error it is definitely not undefined
|
||||
parserOptions: react.languageOptions.parserOptions,
|
||||
|
||||
@@ -19,7 +19,7 @@ export default defineConfig({
|
||||
exclude: [
|
||||
"src/**/*.test.ts",
|
||||
"src/test-utils/**",
|
||||
// Barrel only re-exports algorithms — no executable lines to cover.
|
||||
// Barrel only re-exports algorithms - no executable lines to cover.
|
||||
"src/index.ts",
|
||||
// Type-only / unused by current test surface (Tap nyc also did not require these).
|
||||
"src/util/types.ts",
|
||||
|
||||
@@ -20,9 +20,7 @@ const options = program.opts();
|
||||
const DEBUG = options.debug;
|
||||
|
||||
const db = new Database(options.db, { readonly: true });
|
||||
const dbRows = db
|
||||
.query(`SELECT * FROM Charts WHERE path LIKE '%${options.filter}%'`)
|
||||
.all();
|
||||
const dbRows = db.query(`SELECT * FROM Charts WHERE path LIKE '%${options.filter}%'`).all();
|
||||
console.log(`Found ${dbRows.length} charts.`);
|
||||
|
||||
const songs = ReadCollection("songs-usc.json");
|
||||
|
||||
@@ -35,7 +35,7 @@ function sqlStr(value: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* `jsonb_array_elements` does not work on JSON objects — rewrite legacy elemMatch SQL
|
||||
* `jsonb_array_elements` does not work on JSON objects - rewrite legacy elemMatch SQL
|
||||
* to key lookup (must match `5-folders-to-sql-queries.ts`).
|
||||
*/
|
||||
function rewriteFolderWhereTableFoldersElemExists(where: string): string | null {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# tachi-seeds-webui
|
||||
|
||||
The seeds workstation. Browse, query, diff over git history — and, in
|
||||
The seeds workstation. Browse, query, diff over git history - and, in
|
||||
localdev, edit `db/seeds/*.json` directly.
|
||||
|
||||
## Running
|
||||
|
||||
@@ -54,7 +54,7 @@ export async function listBranches(
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// detached head — leave current null.
|
||||
// detached head - leave current null.
|
||||
}
|
||||
|
||||
return { branches, current };
|
||||
@@ -129,7 +129,7 @@ export async function statusPorcelain(
|
||||
branch = m?.[1] ?? null;
|
||||
continue;
|
||||
}
|
||||
// Porcelain format: " XY path" — strip the two-char status + space.
|
||||
// Porcelain format: " XY path" - strip the two-char status + space.
|
||||
changed.push(line.slice(3));
|
||||
}
|
||||
return { branch, changed, hasUncommitted: changed.length > 0 };
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "./util/git";
|
||||
import { applyPatch, type JsonPatch } from "./util/json-patch";
|
||||
|
||||
// vite-plugin-seeds-dev — mounts /__seeds/* dev-only endpoints against the
|
||||
// vite-plugin-seeds-dev - mounts /__seeds/* dev-only endpoints against the
|
||||
// local repo's db/seeds directory so the seeds-webui can:
|
||||
//
|
||||
// - GET /__seeds/ping feature probe
|
||||
|
||||
@@ -22,7 +22,7 @@ const queryClient = new QueryClient({
|
||||
|
||||
// Load the editing routes lazily via a single dynamic import. The import is
|
||||
// only reached when EDIT_MODE is true; since EDIT_MODE is replaced with the
|
||||
// literal `false` during `vite build`, rollup drops the entire expression —
|
||||
// literal `false` during `vite build`, rollup drops the entire expression -
|
||||
// which means `editRoutes.ts` and every page it references is never emitted
|
||||
// into the prod bundle.
|
||||
type EditRoute = { component: React.ComponentType; path: string };
|
||||
|
||||
@@ -126,14 +126,14 @@ function FieldTable({ fields }: { fields: FieldDelta[] }) {
|
||||
<td className="diff-fields-path mono">{f.path}</td>
|
||||
<td className="diff-fields-before">
|
||||
{f.kind === "added" ? (
|
||||
<span className="text-muted">—</span>
|
||||
<span className="text-muted">-</span>
|
||||
) : (
|
||||
<pre className="mono">{formatValue(f.before)}</pre>
|
||||
)}
|
||||
</td>
|
||||
<td className="diff-fields-after">
|
||||
{f.kind === "removed" ? (
|
||||
<span className="text-muted">—</span>
|
||||
<span className="text-muted">-</span>
|
||||
) : (
|
||||
<pre className="mono">{formatValue(f.after)}</pre>
|
||||
)}
|
||||
|
||||
@@ -91,7 +91,7 @@ export function CommitSparkline({ commits, days = 90 }: CommitSparklineProps) {
|
||||
y={y}
|
||||
>
|
||||
<title>
|
||||
{b.day} — {b.count} commit{b.count === 1 ? "" : "s"}
|
||||
{b.day} - {b.count} commit{b.count === 1 ? "" : "s"}
|
||||
</title>
|
||||
</rect>
|
||||
);
|
||||
|
||||
@@ -39,7 +39,7 @@ interface ResultTableProps {
|
||||
// Pass `expandedContent` for custom content (e.g. a diff); otherwise the
|
||||
// detail is the whole row as pretty-printed JSON.
|
||||
// - `rowDetailMode="inline"` (with `expandedContent`) shows that detail
|
||||
// under every row with no chevron or click — e.g. Bulk edit previews.
|
||||
// under every row with no chevron or click - e.g. Bulk edit previews.
|
||||
export function ResultTable({
|
||||
result,
|
||||
maxRows = 500,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { type z } from "zod";
|
||||
//
|
||||
// Replaces the previous schema-driven field-tree form. Seeds documents are
|
||||
// complex enough (discriminated unions, per-game chart data, etc.) that a
|
||||
// generated form was always going to be leaky — it's cleaner to just edit
|
||||
// generated form was always going to be leaky - it's cleaner to just edit
|
||||
// the raw JSON and let the schema tell you exactly what's wrong.
|
||||
//
|
||||
// Behaviour:
|
||||
|
||||
@@ -7,7 +7,7 @@ import { basicSetup, EditorView } from "codemirror";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
/**
|
||||
* Replaces the default CodeMirror colors in dark mode — `defaultHighlightStyle` uses #708
|
||||
* Replaces the default CodeMirror colors in dark mode - `defaultHighlightStyle` uses #708
|
||||
* (purple) for keywords, which is nearly unreadable on the seeds-webui near-black page.
|
||||
* When `themeType: "dark"`, this becomes the only non-fallback highlighter, so the purple
|
||||
* palette is not used.
|
||||
@@ -33,7 +33,7 @@ const sqlEditorHighlightDark = HighlightStyle.define(
|
||||
{ themeType: "dark" },
|
||||
);
|
||||
|
||||
/** `data-bs-theme` on <html> — must drive CodeMirror's `EditorView.darkTheme` or the caret stays light-theme (black) on a dark page. */
|
||||
/** `data-bs-theme` on <html> - must drive CodeMirror's `EditorView.darkTheme` or the caret stays light-theme (black) on a dark page. */
|
||||
function useBootstrapPageDark(): boolean {
|
||||
const [isDark, setIsDark] = useState(
|
||||
() => document.documentElement.getAttribute("data-bs-theme") !== "light",
|
||||
@@ -64,7 +64,7 @@ export type SqlEditorProps = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
/** Surfaces, caret, and panels — all use Bootstrap CSS variables so light/dark track the page. */
|
||||
/** Surfaces, caret, and panels - all use Bootstrap CSS variables so light/dark track the page. */
|
||||
// Use baseTheme: `&light` / `&dark` are only expanded in buildTheme(..., lightDarkIDs), not in EditorView.theme().
|
||||
const surfaceTheme = EditorView.baseTheme({
|
||||
"&": {
|
||||
@@ -75,7 +75,7 @@ const surfaceTheme = EditorView.baseTheme({
|
||||
fontFeatureSettings: '"liga" 0, "calt" 0',
|
||||
},
|
||||
// Caret: drawSelection defaults to black / #ddd for &dark, but the editor was not getting
|
||||
// `EditorView.darkTheme` — keep an explicit color so the caret always matches body text.
|
||||
// `EditorView.darkTheme` - keep an explicit color so the caret always matches body text.
|
||||
".cm-cursor, .cm-dropCursor": {
|
||||
borderLeft: "1.2px solid var(--bs-body-color)",
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// This is a *build-time* flag set in vite.config.ts. In a production build
|
||||
// (`vite build`) it is the literal `false`, so the bundler tree-shakes every
|
||||
// branch guarded by `EDIT_MODE`. That's how we keep the static prod bundle
|
||||
// absolutely incapable of writing to disk — even if someone self-hosted it.
|
||||
// absolutely incapable of writing to disk - even if someone self-hosted it.
|
||||
//
|
||||
// `hasDevTransport` (see #lib/transport/transport) additionally probes
|
||||
// /__seeds/ping at runtime before enabling the edit UI, in case a dev-built
|
||||
|
||||
@@ -46,7 +46,7 @@ export function fieldDeltas(a: Row, b: Row, prefix = ""): FieldDelta[] {
|
||||
out.push({ before: av, kind: "removed", path });
|
||||
continue;
|
||||
}
|
||||
// Recurse into plain objects (but not arrays — arrays are treated as
|
||||
// Recurse into plain objects (but not arrays - arrays are treated as
|
||||
// atomic scalar values, which is the right call for things like
|
||||
// `tierlistData` or chart `data`).
|
||||
if (
|
||||
@@ -196,7 +196,7 @@ export function singleDocumentDiff(
|
||||
|
||||
export function formatValue(v: unknown): string {
|
||||
if (v === undefined) {
|
||||
return "—";
|
||||
return "-";
|
||||
}
|
||||
if (v === null) {
|
||||
return "null";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export type Row = Record<string, unknown>;
|
||||
|
||||
// Candidate primary keys, in priority order. The first one present on a row is
|
||||
// used. `id` is listed before `songID` because chart documents carry both —
|
||||
// used. `id` is listed before `songID` because chart documents carry both -
|
||||
// we want to key a chart by *its* id, not by the song it belongs to.
|
||||
export const PK_KEYS = [
|
||||
"id",
|
||||
|
||||
@@ -43,7 +43,7 @@ export function schemaForCollection(name: string): z.ZodType<unknown> | null {
|
||||
return SEEDS_BMS_COURSE_DOCUMENT_SCHEMA as z.ZodType<unknown>;
|
||||
}
|
||||
|
||||
// charts-${V3Game}.json — filenames mirror `db/seeds/` and match ALL_GAMES
|
||||
// charts-${V3Game}.json - filenames mirror `db/seeds/` and match ALL_GAMES
|
||||
// keys (e.g. iidx-sp, bms-14k, gitadora-dora). Do not split on the last
|
||||
// hyphen; that would turn iidx-sp into iidx and miss the schema map.
|
||||
if (stripped.startsWith("charts-")) {
|
||||
|
||||
@@ -12,8 +12,8 @@ export type SeedDocSummaryOpts = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Human-readable one-liner for a seeds row — same rules as the collection
|
||||
* browser list (FormatChart for charts with a song, title — artist for songs,
|
||||
* Human-readable one-liner for a seeds row - same rules as the collection
|
||||
* browser list (FormatChart for charts with a song, title - artist for songs,
|
||||
* etc.).
|
||||
*/
|
||||
export function prettySeedDocSummary(
|
||||
@@ -52,7 +52,7 @@ export function prettySeedDocSummary(
|
||||
case "songs": {
|
||||
const title = str(doc.title);
|
||||
const artist = str(doc.artist);
|
||||
return artist ? `${title} — ${artist}` : title;
|
||||
return artist ? `${title} - ${artist}` : title;
|
||||
}
|
||||
case "folders":
|
||||
case "tables": {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getSqlite } from "./client";
|
||||
|
||||
// Fast content hash for JSON payloads. We hash the serialised string because
|
||||
// getCollection returns parsed JSON; this matches what scripts/bundle-current-seeds
|
||||
// stores (sha256(prefix-16) of the raw file bytes — close enough for change detection).
|
||||
// stores (sha256(prefix-16) of the raw file bytes - close enough for change detection).
|
||||
async function hashJson(value: unknown): Promise<string> {
|
||||
const s = JSON.stringify(value);
|
||||
const buf = new TextEncoder().encode(s);
|
||||
@@ -36,7 +36,7 @@ export async function* buildSqliteFromTransport(
|
||||
const name = names[i]!;
|
||||
const rows = await transport.getCollection(name, rev);
|
||||
const hash = await hashJson(rows);
|
||||
// When rev !== undefined we *always* re-ingest — the caller is looking at
|
||||
// When rev !== undefined we *always* re-ingest - the caller is looking at
|
||||
// a specific commit, not the working copy, and we don't cache historical
|
||||
// loads to avoid OPFS bloat.
|
||||
if (rev === undefined && meta[name] === hash) {
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
// Strategy: *per-collection* tables mirror the on-disk files exactly
|
||||
// (so a row in `charts_iidx_sp` corresponds 1:1 to a JSON object in
|
||||
// `db/seeds/charts-iidx-sp.json`). We project the scalar fields we
|
||||
// query on into columns and keep the rest as a JSON blob — SQLite's
|
||||
// query on into columns and keep the rest as a JSON blob - SQLite's
|
||||
// `json_extract` makes that queryable when needed.
|
||||
//
|
||||
// Cross-cutting questions ("how many charts does iidx-sp have across all
|
||||
// versions?") get answered by materialised views built over per-collection
|
||||
// tables. We build the union of charts/songs lazily — i.e. the view DDL
|
||||
// tables. We build the union of charts/songs lazily - i.e. the view DDL
|
||||
// references every `charts_<game>` table that has been created.
|
||||
//
|
||||
// Table name convention: lowercase, '-' -> '_', strip '.json'.
|
||||
|
||||
@@ -77,7 +77,7 @@ export function makeDevTransport(): SeedsTransport {
|
||||
};
|
||||
}
|
||||
|
||||
// Server-Sent Events helper — the dev plugin streams stdout/stderr line-by-line
|
||||
// Server-Sent Events helper - the dev plugin streams stdout/stderr line-by-line
|
||||
// plus a final `exit` event, so we wrap it as an AsyncIterable<RunEvent>.
|
||||
async function* streamSSE<T>(pathname: string, body: unknown): AsyncIterable<T> {
|
||||
const res = await fetch(`${BASE}${pathname}`, {
|
||||
|
||||
@@ -58,7 +58,7 @@ export type JsonPatch = JsonPatchOp[];
|
||||
export interface SeedsTransport {
|
||||
readonly mode: "dev" | "github";
|
||||
|
||||
// Read-side — always available.
|
||||
// Read-side - always available.
|
||||
listCollections(): Promise<CollectionName[]>;
|
||||
listBranches(): Promise<{ branches: Branch[]; current: Branch | null }>;
|
||||
listCommits(opts: { branch?: string; cursor?: string; file?: string }): Promise<CommitPage>;
|
||||
@@ -67,7 +67,7 @@ export interface SeedsTransport {
|
||||
// branch HEAD" in github.
|
||||
getCollection(name: CollectionName, rev?: string): Promise<unknown[]>;
|
||||
|
||||
// Write-side — populated only in dev mode.
|
||||
// Write-side - populated only in dev mode.
|
||||
writeCollection?: (name: CollectionName, patch: JsonPatch) => Promise<void>;
|
||||
runSort?: () => Promise<void>;
|
||||
gitStatus?: () => Promise<GitStatus>;
|
||||
|
||||
@@ -174,7 +174,7 @@ export function Bulk() {
|
||||
}
|
||||
}
|
||||
if (rejections.length > 0) {
|
||||
setError(`Bulk merge rejected — would not satisfy schema:\n${rejections.join("\n")}`);
|
||||
setError(`Bulk merge rejected - would not satisfy schema:\n${rejections.join("\n")}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ export function Collection() {
|
||||
|
||||
{!schema && EDIT_MODE ? (
|
||||
<div className="alert alert-warning mono">
|
||||
No zod schema found for this collection — editing is unavailable.
|
||||
No zod schema found for this collection - editing is unavailable.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ function mergeSongRowsById(baseSongs: Row[], headSongs: Row[]): Map<string, Row>
|
||||
|
||||
// seeds.tachi.ac always points at the canonical upstream. We use this for
|
||||
// every GitHub link in the diff header (commits, compare, PRs). This is a
|
||||
// deliberate hardcode — the dev transport doesn't know what remote the
|
||||
// deliberate hardcode - the dev transport doesn't know what remote the
|
||||
// local clone is pointing at, and for the hosted site it's always this repo.
|
||||
const GITHUB_REPO_URL = "https://github.com/zkldi/Tachi";
|
||||
|
||||
@@ -341,7 +341,7 @@ function CommitHeader({
|
||||
</div>
|
||||
<div className="commit-header-meta">
|
||||
<span>
|
||||
<strong>{commit?.author.name ?? "—"}</strong>
|
||||
<strong>{commit?.author.name ?? "-"}</strong>
|
||||
{commit?.author.email ? (
|
||||
<span className="text-muted"> <{commit.author.email}></span>
|
||||
) : null}
|
||||
|
||||
@@ -94,7 +94,7 @@ async function computeDocPair(draft: Draft): Promise<DocPair | null> {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Draft item — op / time / discard in a slim bar; diff card always visible
|
||||
// Draft item - op / time / discard in a slim bar; diff card always visible
|
||||
// (same id=…, optional pretty title, field table, Show JSON as on /diff).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -272,7 +272,7 @@ export function Drafts() {
|
||||
) : null}
|
||||
</h2>
|
||||
<p className="page-subtitle">
|
||||
Staged edits. Apply to write them to disk — seeds are re-sorted
|
||||
Staged edits. Apply to write them to disk - seeds are re-sorted
|
||||
automatically.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Tachi brand pink — matches the .st1 fill in logo.svg.
|
||||
// Tachi brand pink - matches the .st1 fill in logo.svg.
|
||||
$tachi-pink: #e61c6e;
|
||||
|
||||
// Bootstrap overrides (must come before `@import "bootstrap-scss/bootstrap"`).
|
||||
@@ -31,7 +31,7 @@ $font-weight-bold: 600;
|
||||
|
||||
$min-contrast-ratio: 3;
|
||||
|
||||
// Slightly warmer/cooler grays on each theme — matches the vibe of tachi-client.
|
||||
// Slightly warmer/cooler grays on each theme - matches the vibe of tachi-client.
|
||||
$body-bg-dark: #131313;
|
||||
$body-tertiary-bg-dark: #1a1a1a;
|
||||
$body-secondary-bg-dark: #242424;
|
||||
|
||||
@@ -253,7 +253,7 @@ code {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Git status strip — appears under the header in dev mode.
|
||||
// Git status strip - appears under the header in dev mode.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.git-strip {
|
||||
@@ -519,7 +519,7 @@ code {
|
||||
}
|
||||
}
|
||||
|
||||
// Row-expand column — narrow, center-aligned, no hover bg.
|
||||
// Row-expand column - narrow, center-aligned, no hover bg.
|
||||
.result-col-expand {
|
||||
width: 2rem;
|
||||
padding: 0.35rem 0.25rem 0.35rem 0.5rem;
|
||||
@@ -709,7 +709,7 @@ code {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Skeleton loading placeholder — used anywhere a list of rows is loading.
|
||||
// Skeleton loading placeholder - used anywhere a list of rows is loading.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@keyframes skeleton-shimmer {
|
||||
@@ -791,7 +791,7 @@ code {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ingest toast — pinned bottom-right overlay that reports sqlite build
|
||||
// Ingest toast - pinned bottom-right overlay that reports sqlite build
|
||||
// progress from anywhere in the app. Non-blocking; slides up on mount and
|
||||
// fades out once the database is ready.
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -934,7 +934,7 @@ code {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Diff view — card-per-row with field-level highlighting. Readable over the
|
||||
// Diff view - card-per-row with field-level highlighting. Readable over the
|
||||
// previous ellipsised table.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1296,7 +1296,7 @@ $diff-chg-border: rgba(245, 177, 76, 0.4);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Seed document list — Collection page renderer.
|
||||
// Seed document list - Collection page renderer.
|
||||
//
|
||||
// Each seed document is a collapsible row: a one-line summary header with
|
||||
// optional edit/delete buttons, and a pretty-printed JSON block when open.
|
||||
@@ -1597,7 +1597,7 @@ $diff-chg-border: rgba(245, 177, 76, 0.4);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Floating drafts bar — pinned bottom-left, shown when there are staged edits
|
||||
// Floating drafts bar - pinned bottom-left, shown when there are staged edits
|
||||
// and the user is not already on /drafts.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ import { seedsDevPlugin } from "./dev/vite-plugin-seeds-dev";
|
||||
// In `vite dev` the seedsDevPlugin mounts /__seeds/* endpoints against the
|
||||
// local repo's db/seeds/ so the UI can edit, query git history, and run
|
||||
// the seeds-scripts tests. These endpoints are NOT present in the
|
||||
// production build — all references to them are gated behind
|
||||
// production build - all references to them are gated behind
|
||||
// import.meta.env.VITE_SEEDS_EDIT_MODE and tree-shaken out.
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, "../..");
|
||||
|
||||
/** Hoisted path for a workspace @codemirror/* package (single instance — avoids "Unrecognized extension value" from nested node_modules). */
|
||||
/** Hoisted path for a workspace @codemirror/* package (single instance - avoids "Unrecognized extension value" from nested node_modules). */
|
||||
const cmPackage = (name: string) => path.join(REPO_ROOT, "node_modules", "@codemirror", name);
|
||||
|
||||
const codemirrorSingleInstanceAliases: {
|
||||
@@ -35,7 +35,7 @@ const lezerSingleInstanceAliases: {
|
||||
}));
|
||||
|
||||
export default defineConfig(({ command }) => ({
|
||||
// Edit mode is only on for `vite dev` — `vite build` and `vite preview`
|
||||
// Edit mode is only on for `vite dev` - `vite build` and `vite preview`
|
||||
// produce a read-only bundle suitable for hosting at seeds.tachi.ac.
|
||||
define: {
|
||||
"import.meta.env.VITE_SEEDS_EDIT_MODE": JSON.stringify(command === "serve"),
|
||||
@@ -67,7 +67,7 @@ export default defineConfig(({ command }) => ({
|
||||
// In prod builds, short-circuit modules that are only reachable
|
||||
// when EDIT_MODE is on. `editRoutes` hosts the edit-only pages
|
||||
// and `schemas` pulls in the whole tachi-common game config tree
|
||||
// for zod introspection — we don't want either of those shipping
|
||||
// for zod introspection - we don't want either of those shipping
|
||||
// to seeds.tachi.ac.
|
||||
{
|
||||
name: "seeds-webui:strip-edit-routes",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Dockerfile
|
||||
|
||||
# Env files — mirror repo root `.dockerignore` when this package is build context alone.
|
||||
# Env files - mirror repo root `.dockerignore` when this package is build context alone.
|
||||
.env
|
||||
.env.*
|
||||
**/.env
|
||||
|
||||
@@ -13,7 +13,6 @@ MIGRATIONS_DIR=/tachi/db/migrations
|
||||
LOG_LEVEL=debug
|
||||
|
||||
TACHI_EMAIL_FROM=dev@localhost
|
||||
TACHI_EMAIL_AUTH_POSTMARK=false
|
||||
TACHI_EMAIL_HOST=tachi-mailpit
|
||||
TACHI_EMAIL_PORT=1025
|
||||
TACHI_EMAIL_SECURE=false
|
||||
|
||||
@@ -53,7 +53,6 @@ TACHI_CDN_SAVE_LOCATION_PRIVATE_REGION=
|
||||
|
||||
# Email (required)
|
||||
TACHI_EMAIL_FROM=
|
||||
TACHI_EMAIL_AUTH_POSTMARK=
|
||||
TACHI_EMAIL_HOST=
|
||||
TACHI_EMAIL_PORT=
|
||||
TACHI_EMAIL_SECURE=
|
||||
@@ -69,7 +68,7 @@ TACHI_MYT_API_HOST=
|
||||
TACHI_ARC_AUTH_TOKEN=
|
||||
TACHI_MYT_AUTH_TOKEN=
|
||||
|
||||
# OAuth2 — set all 3 for a provider or none
|
||||
# OAuth2 - set all 3 for a provider or none
|
||||
TACHI_FLO_OAUTH2_CLIENT_ID=
|
||||
TACHI_FLO_OAUTH2_CLIENT_SECRET=
|
||||
TACHI_FLO_OAUTH2_REDIRECT_URI=
|
||||
@@ -80,7 +79,7 @@ TACHI_MIN_OAUTH2_CLIENT_ID=
|
||||
TACHI_MIN_OAUTH2_CLIENT_SECRET=
|
||||
TACHI_MIN_OAUTH2_REDIRECT_URI=
|
||||
|
||||
# CG — set both for a suffix or neither
|
||||
# CG - set both for a suffix or neither
|
||||
TACHI_CG_DEV_API_KEY=
|
||||
TACHI_CG_DEV_URL=
|
||||
TACHI_CG_NAG_API_KEY=
|
||||
@@ -88,7 +87,7 @@ TACHI_CG_NAG_URL=
|
||||
TACHI_CG_GAN_API_KEY=
|
||||
TACHI_CG_GAN_URL=
|
||||
|
||||
# Seeds (optional) — set TACHI_SEEDS_TYPE to LOCAL_FILES or GIT_REPO to enable
|
||||
# Seeds (optional) - set TACHI_SEEDS_TYPE to LOCAL_FILES or GIT_REPO to enable
|
||||
TACHI_SEEDS_TYPE=
|
||||
TACHI_SEEDS_PATH=
|
||||
TACHI_SEEDS_REPO_URL=
|
||||
@@ -96,7 +95,7 @@ TACHI_SEEDS_USER_NAME=
|
||||
TACHI_SEEDS_USER_EMAIL=
|
||||
TACHI_SEEDS_BRANCH=
|
||||
|
||||
# Invite codes — set all 3 or none
|
||||
# Invite codes - set all 3 or none
|
||||
TACHI_INVITE_CODE_BATCH_SIZE=
|
||||
TACHI_INVITE_CODE_INVITE_CAP=
|
||||
TACHI_INVITE_CODE_BETA_USER_BONUS=
|
||||
|
||||
@@ -42,7 +42,6 @@ TACHI_GAME_GROUPS=iidx,museca,sdvx,bms,chunithm,usc,wacca,popn,jubeat,pms,maimai
|
||||
TACHI_IMPORT_TYPES=file/eamusement-iidx-csv,file/batch-manual,file/solid-state-squad,file/pli-iidx-csv,ir/direct-manual,ir/barbatos,ir/fervidex,ir/fervidex-static,ir/beatoraja,ir/usc,ir/kshook-sv6c,api/eag-iidx,api/eag-sdvx,api/flo-iidx,api/flo-sdvx,api/min-sdvx
|
||||
|
||||
TACHI_EMAIL_FROM=test@example.com
|
||||
TACHI_EMAIL_AUTH_POSTMARK=false
|
||||
TACHI_EMAIL_HOST=127.0.0.1
|
||||
TACHI_EMAIL_PORT=1025
|
||||
TACHI_EMAIL_SECURE=false
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
files: "src/test-utils/single-process-tap.ts"
|
||||
test-env:
|
||||
- NODE_ENV=test
|
||||
- PORT=8080
|
||||
- LOG_LEVEL=info
|
||||
# On a local machine, these tests run in under 2-3 minutes.
|
||||
# On github actions, these tests can take an inordinate amount of time.
|
||||
# The timeout is set to 10 minutes, which is ridiculous.
|
||||
# Doesn't matter for real machines.
|
||||
timeout: 600
|
||||
ts: true
|
||||
before: "./src/test-utils/hack-setup.js"
|
||||
check-coverage: true
|
||||
branches: 30
|
||||
functions: 80
|
||||
lines: 80
|
||||
statements: 80
|
||||
reporter: terse
|
||||
@@ -31,6 +31,6 @@ export const ACTION_ResendVerifyEmail = MakeAction("RESEND_VERIFY_EMAIL", async
|
||||
|
||||
const { text, html } = EmailFormatVerifyEmail(taker.acct.username, newToken);
|
||||
|
||||
void SendEmail(verifyInfo.email, "Email Verification", html, text);
|
||||
await SendEmail(verifyInfo.email, "Email Verification", html, text);
|
||||
return {};
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ServerConfig } from "#lib/setup/config";
|
||||
import DB from "#services/pg/db";
|
||||
import { seedInvite, seedUser } from "#test-utils/pg-fixtures";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { ANON_ACTION_Register } from "./register";
|
||||
|
||||
@@ -217,6 +218,52 @@ describe("ANON_ACTION_Register", () => {
|
||||
expect(user).toBeUndefined();
|
||||
});
|
||||
|
||||
// ── Sequence gaps ─────────────────────────────────────────────────────────
|
||||
|
||||
it("does not burn sequence values on failed registrations with bad invite codes", async () => {
|
||||
const first = await ANON_ACTION_Register(taker, {
|
||||
username: "firstuser",
|
||||
"!password": "securepassword",
|
||||
email: "first@example.com",
|
||||
captcha: "test",
|
||||
inviteCode,
|
||||
});
|
||||
|
||||
// Need a fresh invite for the next successful signup.
|
||||
const { id: seedId } = await seedUser({
|
||||
username: "inviter2",
|
||||
email: "inviter2@example.com",
|
||||
});
|
||||
const secondInvite = await seedInvite(seedId, "SECOND_INVITE");
|
||||
|
||||
// 10 failed registrations — bad invite code but unique username/email,
|
||||
// so they enter the transaction and reach AddNewUser before failing.
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await expect(
|
||||
ANON_ACTION_Register(taker, {
|
||||
username: `baduser${i}`,
|
||||
"!password": "securepassword",
|
||||
email: `bad${i}@example.com`,
|
||||
captcha: "test",
|
||||
inviteCode: "BOGUS_CODE",
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
}
|
||||
|
||||
const second = await ANON_ACTION_Register(taker, {
|
||||
username: "seconduser",
|
||||
"!password": "securepassword",
|
||||
email: "second@example.com",
|
||||
captcha: "test",
|
||||
inviteCode: secondInvite,
|
||||
});
|
||||
|
||||
// If sequences are gap-free, the second real user should be first.id + 1
|
||||
// (skipping only the seedUser helper we inserted, which also burns one).
|
||||
// With BIGSERIAL, this FAILS: the 10 rolled-back txns each burned a value.
|
||||
expect(second.userID).toBe(first.userID + 2);
|
||||
});
|
||||
|
||||
// ── Audit log ──────────────────────────────────────────────────────────────
|
||||
|
||||
it("writes a BAD action row when the username is already taken", async () => {
|
||||
@@ -276,3 +323,94 @@ describe("ANON_ACTION_Register", () => {
|
||||
expect(input).not.toHaveProperty("!password");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Bootstrap invite (INVITE_ADMIN_INITIAL_INVITE_CODE) ──────────────────────
|
||||
|
||||
describe("ANON_ACTION_Register - bootstrap invite", () => {
|
||||
const taker = { ip: "127.0.0.1" };
|
||||
const BOOTSTRAP_CODE = "BOOTSTRAP_SECRET_FOR_TESTS";
|
||||
|
||||
let originalBootstrap: typeof ServerConfig.INVITE_ADMIN_INITIAL_INVITE_CODE;
|
||||
|
||||
beforeEach(() => {
|
||||
originalBootstrap = ServerConfig.INVITE_ADMIN_INITIAL_INVITE_CODE;
|
||||
ServerConfig.INVITE_ADMIN_INITIAL_INVITE_CODE = BOOTSTRAP_CODE;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ServerConfig.INVITE_ADMIN_INITIAL_INVITE_CODE = originalBootstrap;
|
||||
});
|
||||
|
||||
it("creates an admin user when the instance has no accounts", async () => {
|
||||
const result = await ANON_ACTION_Register(taker, {
|
||||
username: "firstadmin",
|
||||
"!password": "securepassword",
|
||||
email: "admin@example.com",
|
||||
captcha: "test",
|
||||
inviteCode: BOOTSTRAP_CODE,
|
||||
});
|
||||
|
||||
const account = await DB.selectFrom("account")
|
||||
.select(["account.id", "account.auth_level"])
|
||||
.where("account.id", "=", result.userID)
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
expect(account.auth_level).toBe("admin");
|
||||
});
|
||||
|
||||
it("does not consume any priv_invite row", async () => {
|
||||
await ANON_ACTION_Register(taker, {
|
||||
username: "firstadmin",
|
||||
"!password": "securepassword",
|
||||
email: "admin@example.com",
|
||||
captcha: "test",
|
||||
inviteCode: BOOTSTRAP_CODE,
|
||||
});
|
||||
|
||||
const invites = await DB.selectFrom("priv_invite")
|
||||
.select(DB.fn.countAll().as("count"))
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
expect(Number(invites.count)).toBe(0);
|
||||
});
|
||||
|
||||
it("rejects the bootstrap code when an account already exists", async () => {
|
||||
await seedUser();
|
||||
|
||||
await expect(
|
||||
ANON_ACTION_Register(taker, {
|
||||
username: "seconduser",
|
||||
"!password": "securepassword",
|
||||
email: "second@example.com",
|
||||
captcha: "test",
|
||||
inviteCode: BOOTSTRAP_CODE,
|
||||
}),
|
||||
).rejects.toMatchObject({ code: 400 });
|
||||
});
|
||||
|
||||
it("rejects a wrong code even on an empty instance", async () => {
|
||||
await expect(
|
||||
ANON_ACTION_Register(taker, {
|
||||
username: "firstadmin",
|
||||
"!password": "securepassword",
|
||||
email: "admin@example.com",
|
||||
captcha: "test",
|
||||
inviteCode: "WRONG_CODE",
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("falls through to normal invite validation when env is unset", async () => {
|
||||
ServerConfig.INVITE_ADMIN_INITIAL_INVITE_CODE = undefined;
|
||||
|
||||
await expect(
|
||||
ANON_ACTION_Register(taker, {
|
||||
username: "firstadmin",
|
||||
"!password": "securepassword",
|
||||
email: "admin@example.com",
|
||||
captcha: "test",
|
||||
inviteCode: "NONEXISTENT",
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,8 +7,25 @@ import DB from "#services/pg/db";
|
||||
import { Random20Hex } from "#utils/misc";
|
||||
import { CheckIfEmailInUse, GetUserCaseInsensitive } from "#utils/user";
|
||||
import { ExpectedErr, log } from "bliss";
|
||||
import crypto from "crypto";
|
||||
import { sql } from "kysely";
|
||||
import { type UserDocument } from "tachi-common";
|
||||
|
||||
/** Namespace for the advisory lock that serialises bootstrap-invite registrations. */
|
||||
const BOOTSTRAP_INVITE_ADVISORY_KEY1 = 0x42_6f_6f_74; // "Boot"
|
||||
const BOOTSTRAP_INVITE_ADVISORY_KEY2 = 0x49_6e_76_74; // "Invt"
|
||||
|
||||
function bootstrapCodeMatches(input: string, expected: string): boolean {
|
||||
const a = Buffer.from(input, "utf8");
|
||||
const b = Buffer.from(expected, "utf8");
|
||||
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return crypto.timingSafeEqual(a, b);
|
||||
}
|
||||
|
||||
export const ANON_ACTION_Register = MakeAnonAction(
|
||||
"REGISTER",
|
||||
async (taker, { email, "!password": password, inviteCode, username, captcha }) => {
|
||||
@@ -44,10 +61,6 @@ export const ANON_ACTION_Register = MakeAnonAction(
|
||||
}
|
||||
|
||||
const newUser = await DB.transaction().execute(async (txn): Promise<UserDocument> => {
|
||||
// if we get to this point, We're good to create the user.
|
||||
|
||||
const { newUser, newSettings: _ } = await AddNewUser(txn, username, password, email);
|
||||
|
||||
if (ServerConfig.INVITE_CODE_CONFIG) {
|
||||
if (!inviteCode) {
|
||||
throw new ExpectedErr(
|
||||
@@ -55,20 +68,61 @@ export const ANON_ACTION_Register = MakeAnonAction(
|
||||
"No invite code given, yet the server uses invites.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const isBootstrapInvite =
|
||||
ServerConfig.INVITE_CODE_CONFIG &&
|
||||
ServerConfig.INVITE_ADMIN_INITIAL_INVITE_CODE &&
|
||||
inviteCode !== null &&
|
||||
bootstrapCodeMatches(inviteCode, ServerConfig.INVITE_ADMIN_INITIAL_INVITE_CODE);
|
||||
|
||||
if (isBootstrapInvite) {
|
||||
await sql`SELECT pg_advisory_xact_lock(${BOOTSTRAP_INVITE_ADVISORY_KEY1}, ${BOOTSTRAP_INVITE_ADVISORY_KEY2})`.execute(
|
||||
txn,
|
||||
);
|
||||
|
||||
const { count } = await txn
|
||||
.selectFrom("account")
|
||||
.select(txn.fn.countAll().as("count"))
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
if (Number(count) > 0) {
|
||||
log.info("Bootstrap invite used but instance already has users.");
|
||||
throw new ExpectedErr(400, `Invalid invite code given: ${inviteCode}.`);
|
||||
}
|
||||
} else if (ServerConfig.INVITE_CODE_CONFIG) {
|
||||
// Validate the invite code BEFORE creating the user so a bad code
|
||||
// never burns a sequence value on `account.id`.
|
||||
// FOR UPDATE locks the row so a concurrent signup can't consume
|
||||
// the same code between our check and the update below.
|
||||
const inviteCodeDoc = await txn
|
||||
.selectFrom("priv_invite")
|
||||
.select("code")
|
||||
.where("code", "=", inviteCode)
|
||||
.where("consumed", "=", false)
|
||||
.select("priv_invite.code")
|
||||
.where("priv_invite.code", "=", inviteCode!)
|
||||
.where("priv_invite.consumed", "=", false)
|
||||
.forUpdate()
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!inviteCodeDoc) {
|
||||
log.info(`Invalid invite code given: ${inviteCode}.`);
|
||||
throw new ExpectedErr(400, `Invalid invite code given: ${inviteCode}.`);
|
||||
}
|
||||
}
|
||||
|
||||
log.info(`Consumed invite ${inviteCodeDoc.code}.`);
|
||||
const { newUser, newSettings: _ } = await AddNewUser(txn, username, password, email);
|
||||
|
||||
if (isBootstrapInvite) {
|
||||
await txn
|
||||
.updateTable("account")
|
||||
.set({ auth_level: "admin" })
|
||||
.where("account.id", "=", newUser.id)
|
||||
.execute();
|
||||
|
||||
log.info(
|
||||
`Bootstrap invite consumed — user ${newUser.username} (${newUser.id}) is now admin.`,
|
||||
);
|
||||
} else if (ServerConfig.INVITE_CODE_CONFIG) {
|
||||
log.info(`Consumed invite ${inviteCode}.`);
|
||||
|
||||
await txn
|
||||
.updateTable("priv_invite")
|
||||
@@ -77,7 +131,7 @@ export const ANON_ACTION_Register = MakeAnonAction(
|
||||
consumed_at: new Date().toISOString(),
|
||||
consumed_by: newUser.id,
|
||||
})
|
||||
.where("code", "=", inviteCode)
|
||||
.where("priv_invite.code", "=", inviteCode!)
|
||||
.execute();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { loadServerEnvFile } from "#lib/setup/load-server-env";
|
||||
loadServerEnvFile(process.env.NODE_ENV === "test" ? ".env.test" : ".env");
|
||||
|
||||
@@ -5,8 +6,12 @@ import { runCronTickOnce } from "#lib/jobs/cron/cron-service";
|
||||
import { log } from "#lib/log/log";
|
||||
import { Env } from "#lib/setup/config";
|
||||
import { ClosePgConnection } from "#services/pg/db";
|
||||
import { Sleep } from "#utils/misc";
|
||||
import { writeFileSync } from "fs";
|
||||
import { applyMigrations } from "tachi-db-migration-engine";
|
||||
|
||||
const HEARTBEAT_FILE = "/tmp/worker-heartbeat";
|
||||
|
||||
const TICK_MS = 5_000;
|
||||
|
||||
process.on("uncaughtException", (err, origin) => {
|
||||
@@ -23,6 +28,7 @@ void bootstrap();
|
||||
async function bootstrap() {
|
||||
await applyMigrations(Env.POSTGRES_URL, Env.MIGRATIONS_DIR);
|
||||
log.info({ bootInfo: true }, "tachi cron worker starting.");
|
||||
|
||||
let stopping = false;
|
||||
const shutdown = () => {
|
||||
stopping = true;
|
||||
@@ -32,8 +38,8 @@ async function bootstrap() {
|
||||
|
||||
// eslint-disable-next-line no-unmodified-loop-condition
|
||||
while (!stopping) {
|
||||
writeFileSync(HEARTBEAT_FILE, Date.now().toString());
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await runCronTickOnce();
|
||||
} catch (e) {
|
||||
log.error(e, "Cron tick error.");
|
||||
@@ -41,10 +47,7 @@ async function bootstrap() {
|
||||
if (stopping) {
|
||||
break;
|
||||
}
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((r) => {
|
||||
setTimeout(r, TICK_MS);
|
||||
});
|
||||
await Sleep(TICK_MS);
|
||||
}
|
||||
log.info("Cron worker stopped.");
|
||||
await ClosePgConnection();
|
||||
|
||||
@@ -150,7 +150,7 @@ describe("DDR profileCalcs (flareSkill, Postgres)", () => {
|
||||
|
||||
it("counts at most 30 PBs per flare category (global flare order, per-category index)", async () => {
|
||||
const { id: userId } = await seedUser();
|
||||
// 31 CLASSIC rows, flareSkill 1031..1001 — sorted desc; index 30 is dropped (top < 30 keeps 0..29).
|
||||
// 31 CLASSIC rows, flareSkill 1031..1001 - sorted desc; index 30 is dropped (top < 30 keeps 0..29).
|
||||
for (let i = 0; i < 31; i++) {
|
||||
await seedDdrPbRow(userId, {
|
||||
flareSkill: 1031 - i,
|
||||
|
||||
@@ -236,7 +236,7 @@ describe("IIDX_IMPL (unit)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("scoreCalcs ktLampRatingNC / ktLampRatingHC / ktLampRatingEXHC", () => {
|
||||
describe("scoreCalcs ktLampRatingHC / ktLampRatingEXHC", () => {
|
||||
it("IIDX SP", () => {
|
||||
const run = (
|
||||
scoreData: Partial<ScoreData<"iidx-sp">>,
|
||||
@@ -258,37 +258,30 @@ describe("IIDX_IMPL (unit)", () => {
|
||||
const tiered = { ncTier: mkTier(15), hcTier: mkTier(16), exhcTier: mkTier(17) };
|
||||
|
||||
expect(run({ lamp: "FAILED" }, {})).toMatchObject({
|
||||
ktLampRatingNC: 0,
|
||||
ktLampRatingHC: 0,
|
||||
ktLampRatingEXHC: 0,
|
||||
});
|
||||
expect(run({ lamp: "EASY CLEAR" }, {})).toMatchObject({
|
||||
ktLampRatingNC: 0,
|
||||
ktLampRatingHC: 0,
|
||||
ktLampRatingEXHC: 0,
|
||||
});
|
||||
expect(run({ lamp: "CLEAR" }, tiered)).toMatchObject({
|
||||
ktLampRatingNC: 15,
|
||||
ktLampRatingHC: 0,
|
||||
ktLampRatingEXHC: 0,
|
||||
});
|
||||
expect(run({ lamp: "HARD CLEAR" }, tiered)).toMatchObject({
|
||||
ktLampRatingNC: 15,
|
||||
ktLampRatingHC: 16,
|
||||
ktLampRatingEXHC: 0,
|
||||
});
|
||||
expect(run({ lamp: "EX HARD CLEAR" }, tiered)).toMatchObject({
|
||||
ktLampRatingNC: 15,
|
||||
ktLampRatingHC: 16,
|
||||
ktLampRatingEXHC: 17,
|
||||
});
|
||||
expect(run({ lamp: "FULL COMBO" }, tiered)).toMatchObject({
|
||||
ktLampRatingNC: 15,
|
||||
ktLampRatingHC: 16,
|
||||
ktLampRatingEXHC: 17,
|
||||
});
|
||||
expect(run({ lamp: "HARD CLEAR" }, { ncTier: mkTier(15) })).toMatchObject({
|
||||
ktLampRatingNC: 15,
|
||||
ktLampRatingHC: 15,
|
||||
ktLampRatingEXHC: 0,
|
||||
});
|
||||
@@ -313,27 +306,22 @@ describe("IIDX_IMPL (unit)", () => {
|
||||
}
|
||||
|
||||
expect(run({ lamp: "EASY CLEAR" }, { dpTier: mkTier(15) })).toMatchObject({
|
||||
ktLampRatingNC: 0,
|
||||
ktLampRatingHC: 0,
|
||||
ktLampRatingEXHC: 0,
|
||||
});
|
||||
expect(run({ lamp: "CLEAR" }, { dpTier: mkTier(15) })).toMatchObject({
|
||||
ktLampRatingNC: 15,
|
||||
ktLampRatingHC: 0,
|
||||
ktLampRatingEXHC: 0,
|
||||
});
|
||||
expect(run({ lamp: "HARD CLEAR" }, { dpTier: mkTier(15) })).toMatchObject({
|
||||
ktLampRatingNC: 15,
|
||||
ktLampRatingHC: 15,
|
||||
ktLampRatingEXHC: 0,
|
||||
});
|
||||
expect(run({ lamp: "EX HARD CLEAR" }, { dpTier: mkTier(15) })).toMatchObject({
|
||||
ktLampRatingNC: 15,
|
||||
ktLampRatingHC: 15,
|
||||
ktLampRatingEXHC: 15,
|
||||
});
|
||||
expect(run({ lamp: "FULL COMBO" }, { dpTier: mkTier(15) })).toMatchObject({
|
||||
ktLampRatingNC: 15,
|
||||
ktLampRatingHC: 15,
|
||||
ktLampRatingEXHC: 15,
|
||||
});
|
||||
|
||||
@@ -29,23 +29,19 @@ type IIDXGames = "iidx-dp" | "iidx-sp";
|
||||
const IIDX_SESSION_CALCS: GPTSessionCalcs<IIDXGames> = (arr) => ({
|
||||
BPI: SessionAvgBest10For("BPI")(arr),
|
||||
ktLampRating: SessionAvgBest10For("ktLampRating")(arr),
|
||||
ktLampRatingNC: SessionAvgBest10For("ktLampRatingNC")(arr),
|
||||
ktLampRatingHC: SessionAvgBest10For("ktLampRatingHC")(arr),
|
||||
ktLampRatingEXHC: SessionAvgBest10For("ktLampRatingEXHC")(arr),
|
||||
});
|
||||
|
||||
const IIDX_PROFILE_CALCS: GPTProfileCalcs<IIDXGames> = async (game, userID) => {
|
||||
const [BPI, ktLampRating, ktLampRatingNC, ktLampRatingHC, ktLampRatingEXHC] = await Promise.all(
|
||||
[
|
||||
ProfileAvgBestN("BPI", 20, true)(game, userID),
|
||||
ProfileAvgBestN("ktLampRating", 20)(game, userID),
|
||||
ProfileAvgBestN("ktLampRatingNC", 20)(game, userID),
|
||||
ProfileAvgBestN("ktLampRatingHC", 20)(game, userID),
|
||||
ProfileAvgBestN("ktLampRatingEXHC", 20)(game, userID),
|
||||
],
|
||||
);
|
||||
const [BPI, ktLampRating, ktLampRatingHC, ktLampRatingEXHC] = await Promise.all([
|
||||
ProfileAvgBestN("BPI", 20, true)(game, userID),
|
||||
ProfileAvgBestN("ktLampRating", 20)(game, userID),
|
||||
ProfileAvgBestN("ktLampRatingHC", 20)(game, userID),
|
||||
ProfileAvgBestN("ktLampRatingEXHC", 20)(game, userID),
|
||||
]);
|
||||
|
||||
return { BPI, ktLampRating, ktLampRatingNC, ktLampRatingHC, ktLampRatingEXHC };
|
||||
return { BPI, ktLampRating, ktLampRatingHC, ktLampRatingEXHC };
|
||||
};
|
||||
|
||||
const IIDX_MERGERS: Array<PBMergeFunction<IIDXGames>> = [
|
||||
@@ -156,11 +152,6 @@ export const IIDX_SP_IMPL: GameImplementation<"iidx-sp"> = {
|
||||
ktLampRating = 0;
|
||||
}
|
||||
|
||||
const atLeastNcClear =
|
||||
scoreData.lamp === "CLEAR" ||
|
||||
scoreData.lamp === "HARD CLEAR" ||
|
||||
scoreData.lamp === "EX HARD CLEAR" ||
|
||||
scoreData.lamp === "FULL COMBO";
|
||||
const atLeastHcClear =
|
||||
scoreData.lamp === "HARD CLEAR" ||
|
||||
scoreData.lamp === "EX HARD CLEAR" ||
|
||||
@@ -168,14 +159,12 @@ export const IIDX_SP_IMPL: GameImplementation<"iidx-sp"> = {
|
||||
const atLeastExhcClear =
|
||||
scoreData.lamp === "EX HARD CLEAR" || scoreData.lamp === "FULL COMBO";
|
||||
|
||||
const ktLampRatingNC = atLeastNcClear ? ncValue : 0;
|
||||
const ktLampRatingHC = atLeastHcClear ? hcValue : 0;
|
||||
const ktLampRatingEXHC = atLeastExhcClear ? exhcValue : 0;
|
||||
|
||||
return {
|
||||
BPI: bpi,
|
||||
ktLampRating,
|
||||
ktLampRatingNC,
|
||||
ktLampRatingHC,
|
||||
ktLampRatingEXHC,
|
||||
};
|
||||
@@ -235,11 +224,6 @@ export const IIDX_DP_IMPL: GameImplementation<"iidx-dp"> = {
|
||||
ktLampRating = 0;
|
||||
}
|
||||
|
||||
const atLeastNcClear =
|
||||
scoreData.lamp === "CLEAR" ||
|
||||
scoreData.lamp === "HARD CLEAR" ||
|
||||
scoreData.lamp === "EX HARD CLEAR" ||
|
||||
scoreData.lamp === "FULL COMBO";
|
||||
const atLeastHcClear =
|
||||
scoreData.lamp === "HARD CLEAR" ||
|
||||
scoreData.lamp === "EX HARD CLEAR" ||
|
||||
@@ -247,14 +231,12 @@ export const IIDX_DP_IMPL: GameImplementation<"iidx-dp"> = {
|
||||
const atLeastExhcClear =
|
||||
scoreData.lamp === "EX HARD CLEAR" || scoreData.lamp === "FULL COMBO";
|
||||
|
||||
const ktLampRatingNC = atLeastNcClear ? ecValue : 0;
|
||||
const ktLampRatingHC = atLeastHcClear ? ecValue : 0;
|
||||
const ktLampRatingEXHC = atLeastExhcClear ? ecValue : 0;
|
||||
|
||||
return {
|
||||
BPI: bpi,
|
||||
ktLampRating,
|
||||
ktLampRatingNC,
|
||||
ktLampRatingHC,
|
||||
ktLampRatingEXHC,
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ import { GoalFmtPercent, GradeGoalFormatter } from "./_common";
|
||||
export const ITG_STAMINA_IMPL: GameImplementation<"itg-stamina"> = {
|
||||
chartSpecificValidators: {},
|
||||
scoreDeriver: (scoreData, _chart) => ({
|
||||
// *important*: don't check survivedPercent === 100 — floating point can
|
||||
// *important*: don't check survivedPercent === 100 - floating point can
|
||||
// produce a 100% survived-percent on a fail for very long charts.
|
||||
finalPercent:
|
||||
scoreData.lamp === "FAILED" ? scoreData.survivedPercent : 100 + scoreData.scorePercent,
|
||||
|
||||
@@ -60,7 +60,6 @@ describe("CreatePBMergeFor (Postgres)", () => {
|
||||
judgements: s.mongo.judgements ?? {},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await DB.insertInto("score")
|
||||
.values({
|
||||
id: scoreId,
|
||||
|
||||
@@ -10,8 +10,12 @@ import { processScoreImportJobFromPayload } from "#lib/score-import/worker/score
|
||||
import { Env } from "#lib/setup/config";
|
||||
import { ClosePgConnection } from "#services/pg/db";
|
||||
import { CloseRedisConnection } from "#services/redis/redis";
|
||||
import { Sleep } from "#utils/misc";
|
||||
import { writeFileSync } from "fs";
|
||||
import { applyMigrations } from "tachi-db-migration-engine";
|
||||
|
||||
const HEARTBEAT_FILE = "/tmp/worker-heartbeat";
|
||||
|
||||
const POLL_MS = 250;
|
||||
|
||||
process.on("uncaughtException", (err, origin) => {
|
||||
@@ -21,31 +25,32 @@ process.on("uncaughtException", (err, origin) => {
|
||||
|
||||
void bootstrap();
|
||||
|
||||
/**
|
||||
* Often run by `just server` (one or more via `TACHI_SERVER_JOB_WORKER_COUNT`); each process claims
|
||||
* with `FOR UPDATE SKIP LOCKED`.
|
||||
*/
|
||||
async function bootstrap() {
|
||||
await applyMigrations(Env.POSTGRES_URL, Env.MIGRATIONS_DIR);
|
||||
log.info({ bootInfo: true }, "tachi job-queue worker starting (Postgres job_queue).");
|
||||
|
||||
let stopping = false;
|
||||
const shutdown = () => {
|
||||
stopping = true;
|
||||
};
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
|
||||
// eslint-disable-next-line no-unmodified-loop-condition
|
||||
while (!stopping) {
|
||||
writeFileSync(HEARTBEAT_FILE, Date.now().toString());
|
||||
|
||||
const job = await ClaimNextJob();
|
||||
|
||||
if (!job) {
|
||||
if (stopping) {
|
||||
break;
|
||||
}
|
||||
await new Promise((r) => {
|
||||
setTimeout(r, POLL_MS);
|
||||
});
|
||||
|
||||
await Sleep(POLL_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (job.job_kind) {
|
||||
case JOB_KIND_SCORE_IMPORT:
|
||||
@@ -61,6 +66,7 @@ async function bootstrap() {
|
||||
await MarkJobFailed(job.row_id);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Job worker loop stopped, closing resources.");
|
||||
await CloseScoreImportQueue();
|
||||
await CloseRedisConnection();
|
||||
|
||||
@@ -38,7 +38,7 @@ export type ActivityConstraint = {
|
||||
userID?: integer | { $in: Array<integer> };
|
||||
};
|
||||
|
||||
/** Kysely dynamic column refs — same pattern as `whereUserIdOnGoalSub` in `#utils/db`. */
|
||||
/** Kysely dynamic column refs - same pattern as `whereUserIdOnGoalSub` in `#utils/db`. */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OperandExpressionFactory varies by query (session / join / class_achievement).
|
||||
type ActivityWhereEb = any;
|
||||
|
||||
|
||||
@@ -92,13 +92,13 @@ export const SELECT_PB_DOCUMENT_WITH_LEADERBOARD = [
|
||||
export async function ToPbScoreDocument(row: PbDocumentJoinRow): Promise<PBScoreDocument> {
|
||||
const composedRows = await DB.selectFrom("pb_composed_from")
|
||||
.where("pb_id", "=", row.row_id)
|
||||
.select("score_id")
|
||||
.select(["score_id", "merge_name"])
|
||||
.execute();
|
||||
|
||||
const composedFrom: [PBReference, ...PBReference[]] =
|
||||
composedRows.length > 0
|
||||
? (composedRows.map((c, i) => ({
|
||||
name: i === 0 ? "Primary" : `Merge${i}`,
|
||||
? (composedRows.map((c) => ({
|
||||
name: c.merge_name,
|
||||
scoreID: c.score_id,
|
||||
})) as [PBReference, ...PBReference[]])
|
||||
: [{ name: "Primary", scoreID: "unknown" }];
|
||||
|
||||
@@ -23,7 +23,7 @@ try {
|
||||
} else {
|
||||
transporter.verify((err) => {
|
||||
if (err) {
|
||||
// Do NOT throw here — this is an async callback and the throw would
|
||||
// Do NOT throw here - this is an async callback and the throw would
|
||||
// become an uncaught exception, crashing the process. Log and exit instead.
|
||||
log.fatal({ err }, `Could not connect to email server.`);
|
||||
process.exit(1);
|
||||
@@ -65,7 +65,7 @@ export function SendEmail(
|
||||
headers: transporter.options.headers,
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
log.info(
|
||||
log.error(
|
||||
{
|
||||
err,
|
||||
subject,
|
||||
@@ -74,5 +74,6 @@ export function SendEmail(
|
||||
},
|
||||
`Failed to send email to ${to}.`,
|
||||
);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { type Kysely, sql } from "kysely";
|
||||
|
||||
/**
|
||||
* Folder membership SQL (see tests in `folders.test.ts`).
|
||||
* Does not import the global `DB` module — safe for scripts that only set `POSTGRES_URL`.
|
||||
* Does not import the global `DB` module - safe for scripts that only set `POSTGRES_URL`.
|
||||
*/
|
||||
export async function BuildFolderQuery(folderID: string, db: Kysely<Database>) {
|
||||
const folder = await db
|
||||
@@ -75,7 +75,7 @@ export async function GetFolderChartIDs(folderID: string, db: Kysely<Database>)
|
||||
|
||||
/**
|
||||
* Evaluates `folder.where` SQL (same as a full rebuild). Used by
|
||||
* {@link rebuildFolderChartLookup}; do not call for normal reads — use {@link GetFolderChartIDs}.
|
||||
* {@link rebuildFolderChartLookup}; do not call for normal reads - use {@link GetFolderChartIDs}.
|
||||
*/
|
||||
export async function computeFolderChartIdsFromFolderSql(folderID: string, db: Kysely<Database>) {
|
||||
const { folderQuery } = await BuildFolderQuery(folderID, db);
|
||||
|
||||
@@ -46,7 +46,7 @@ function buildList(): Array<CronTaskDef> {
|
||||
},
|
||||
{
|
||||
id: "drain_stats_queues",
|
||||
schedule: "* * * * *",
|
||||
schedule: "* * * * *", // TODO(zk): really? this is so lazy
|
||||
description:
|
||||
"Drain score_rederive, pb_dirty, session_dirty, game_profile_dirty (ordered)",
|
||||
run: drainStatsQueuesInOrder,
|
||||
|
||||
@@ -291,7 +291,7 @@ export async function drainStatsQueuesInOrder(): Promise<void> {
|
||||
/**
|
||||
* Drain `score_rederive`, then `pb_dirty`, `session_dirty`, and `game_profile_dirty`,
|
||||
* repeating until a full pass moves nothing. No per-tick row cap (unlike the cron
|
||||
* drain) — intended for admin synchronous recalc.
|
||||
* drain) - intended for admin synchronous recalc.
|
||||
*/
|
||||
export async function drainStatsQueuesFully(): Promise<void> {
|
||||
for (;;) {
|
||||
@@ -345,7 +345,7 @@ export async function drainStatsQueuesFully(): Promise<void> {
|
||||
|
||||
/**
|
||||
* Drain `pb_dirty` then `session_dirty` and `game_profile_dirty`, repeating until
|
||||
* idle. No per-tick row cap — intended for admin synchronous PB recalc.
|
||||
* idle. No per-tick row cap - intended for admin synchronous PB recalc.
|
||||
*/
|
||||
export async function drainPbDirtyAndDownstream(): Promise<void> {
|
||||
for (;;) {
|
||||
|
||||
@@ -192,7 +192,7 @@ export class TypedRouter<S extends AnyRouterSpec> {
|
||||
if (!outputParseResult.success) {
|
||||
log.error(
|
||||
{ route, issues: outputParseResult.error.issues },
|
||||
"TypedRouter: output validation failed — handler returned body not matching spec output schema",
|
||||
"TypedRouter: output validation failed - handler returned body not matching spec output schema",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ export async function deleteOrphanScoreForUser(
|
||||
|
||||
/**
|
||||
* Lists orphan_score rows for a user, newest first, with keyset pagination on `row_id` + `time_inserted`.
|
||||
* @param afterRowID — `row_id` of the last item from the previous page (omit on first page).
|
||||
* @param afterRowID - `row_id` of the last item from the previous page (omit on first page).
|
||||
*/
|
||||
export async function listOrphanScoresForUser(opts: {
|
||||
afterRowID?: string;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user