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 && (
-