diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 27d43a0..bdf66bb 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,7 +1,7 @@ name: deploy # On push to main: tests run on the VPS via a self-hosted runner; if -# they pass, the backend image is built on GitHub-hosted infra and +# they pass, the backend image is built and # pushed to GHCR; the VPS-side runner then pulls the image and # restarts the docker-compose stack in place. # @@ -19,25 +19,8 @@ env: BACKEND_IMAGE: ${{ github.repository }}-backend jobs: - # PR-only quick test on GitHub-hosted infra. - test-pr: - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version-file: backend/go.mod - cache-dependency-path: backend/go.sum - - name: vet - working-directory: backend - run: go vet ./... - - name: test - working-directory: backend - run: go test ./... - - # Real gate for deploys: tests run on the VPS itself. - test-on-vps: + # run tests before deploy. + test: if: github.event_name == 'push' runs-on: [self-hosted, chessrepeat-vps] steps: @@ -55,7 +38,7 @@ jobs: build-and-push: if: github.event_name == 'push' - needs: test-on-vps + needs: test runs-on: ubuntu-latest permissions: contents: read diff --git a/backend/internal/ws/server.go b/backend/internal/ws/server.go index 4403d49..bb31573 100644 --- a/backend/internal/ws/server.go +++ b/backend/internal/ws/server.go @@ -98,10 +98,9 @@ type subscriber struct { // burn CPU/DB by flooding the connection. Allocated per-connection // so one chatty peer can't starve another. readBudget *ratelimit.Bucket - // lastSeenAt is the unix-nano timestamp of the most recent inbound - // message (any type — pings, mutations, anything). The liveness - // watcher kicks the subscriber if this falls outside idleTimeout. - // atomic because reader (writes) and watcher (reads) run concurrently. + // updated after client sends an operation to server + // if currentTime - `lastSeenAt` < `livenessCheckInterval` after timer fires, + // kick subscriber lastSeenAt atomic.Int64 } diff --git a/frontend/src/Chessrepeat.tsx b/frontend/src/Chessrepeat.tsx index d939649..47dd360 100644 --- a/frontend/src/Chessrepeat.tsx +++ b/frontend/src/Chessrepeat.tsx @@ -223,7 +223,7 @@ export const Chessrepeat = () => { The current move we're training */ - //TODO Fix logic here.. + //TODO Fix logic here.. const targetDest = (): Key[] => { // console.log("selectedNode fen", selectedNode?.data.fen) const targetNode = useTrainerStore.getState().trainableContext.targetMove; @@ -356,7 +356,8 @@ export const Chessrepeat = () => { switch (guess(san)) { case 'success': { const secsUntilDue = train(true); - showBoxAtSquare(to, secsUntilDue); + // null when the move was deleted under us (train resynced instead) + if (secsUntilDue != null) showBoxAtSquare(to, secsUntilDue); setNextTrainablePosition(); break; } @@ -375,7 +376,7 @@ export const Chessrepeat = () => { } }; - const onAfterMove = (from: Key, to: Key, meta: MoveMetadata) => { + const onAfterMove = async (from: Key, to: Key, meta: MoveMetadata) => { const fenBefore = selectedNode?.data.fen || initial; // If a promo is already open, ignore additional moves (defensive) @@ -390,7 +391,7 @@ export const Chessrepeat = () => { // Normal move const san = chessgroundToSan(fenBefore, from, to); - finishMove(san, meta, to); + await finishMove(san, meta, to); updateDueCounts(); }; @@ -426,84 +427,89 @@ export const Chessrepeat = () => { {/* BOARD */}
- {chapter && chapter.enabledCount > 0 && ( -
-
-
-
-
+ {chapter && chapter.enabledCount > 0 && ( +
+
+
+
+
- {/* Breakdown tooltip on hover */} -
-
- - To learn - {chapter.unseenCount} -
-
- - Due - {chapter.lastDueCount} -
-
- - Learned - - {chapter.enabledCount - chapter.unseenCount - chapter.lastDueCount} - -
-
- Total - {chapter.enabledCount} + > + + + + + + + + + + + + + + + +
+ + To Learn + {chapter.unseenCount} + + {Math.round((chapter.unseenCount / chapter.enabledCount) * 100)}% +
+ + Due Now{chapter.lastDueCount} + {Math.round((chapter.lastDueCount / chapter.enabledCount) * 100)}% +
-
- )} -
- - {pendingPromo && ( - + { - const { fenBefore, from, to, meta } = pendingPromo; - closePromo(); - const san = chessgroundToSan(fenBefore, from, to, role); - finishMove(san, meta, to); + fen={selectedNode?.data.fen || initial} + turnColor={turn} + lastMove={lastMove} + movable={{ + free: false, + color: turn, + dests: calculateDests(), + events: { after: onAfterMove }, }} + drawable={{ autoShapes: createShapes() }} /> - )} -
+ {pendingPromo && ( + { + const { fenBefore, from, to, meta } = pendingPromo; + closePromo(); + const san = chessgroundToSan(fenBefore, from, to, role); + finishMove(san, meta, to); + }} + /> + )} +
{/* CONTROLS — part of the same board panel, always directly diff --git a/frontend/src/components/Debug.tsx b/frontend/src/components/Debug.tsx index 988b83f..7e45fa1 100644 --- a/frontend/src/components/Debug.tsx +++ b/frontend/src/components/Debug.tsx @@ -10,9 +10,14 @@ export const Debug: React.FC = () => { const selectedPath = useTrainerStore.getState().selectedPath; const TrainingMethod = useTrainerStore.getState().trainingMethod; + const { repertoire, selectedChapterId } = useTrainerStore.getState(); + + const chapter = repertoire.find((c) => c.uuid === selectedChapterId); + if (!chapter) return undefined; console.log(targetMove?.data?.fen); return (
+
{`chapter ${chapter.unseenCount}`}
{`TrainingPath ${trainingPath}`}
{`selectedPath ${selectedPath}`}
{`TrainingMethod ${TrainingMethod}`}
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 1ad9873..35b5d04 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -135,7 +135,12 @@ export function Header({ connectedUsers, incomingCollaboratorsCount = 0, onOpenC ) : ( showSignIn && ( - diff --git a/frontend/src/components/MobileCommentPopout.tsx b/frontend/src/components/MobileCommentPopout.tsx index 397a8b3..e83364a 100644 --- a/frontend/src/components/MobileCommentPopout.tsx +++ b/frontend/src/components/MobileCommentPopout.tsx @@ -73,7 +73,7 @@ const MobileCommentPopout: React.FC = () => {