Compare commits

...

10 Commits

Author SHA1 Message Date
semantic-release-bot
da53e084f0 chore(release): 4.14.4 [skip ci]
## [4.14.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.3...v4.14.4) (2025-10-19)

### Bug Fixes

* **arcade:** add host-only game selection with clear messaging ([22df1b0](22df1b0b66))
* **arcade:** add host-only game selection with clear messaging ([c0680ca](c0680cad0f))
2025-10-19 17:00:53 +00:00
Thomas Hallock
22df1b0b66 fix(arcade): add host-only game selection with clear messaging
Only room hosts can select games. Added clear visual messaging:
- Host: "👑 You're the room host. Select a game to start playing."
- Non-host: " Waiting for [Host Name] to select a game..."
- Error: "⚠️ Only the room host can select a game. Ask [Host] to choose."

Changes:
- Detect host status via currentMember?.isCreator
- Disable game buttons for non-hosts (opacity 0.4, cursor not-allowed)
- Client-side permission check before API call
- Error messages auto-dismiss after 5 seconds
- Error handling in setRoomGame mutation callback

Fixes 403 errors when non-hosts attempt game selection.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 11:59:43 -05:00
Thomas Hallock
c0680cad0f fix(arcade): add host-only game selection with clear messaging
Non-host room members were getting 403 errors when trying to select games.
Added proper UI restrictions and messaging to clarify only the host can
select games.

**Changes**:

1. **Host Detection**: Check if current user is room creator
   - Find `currentMember` in `roomData.members`
   - Check `isCreator` flag

2. **Visual Restrictions**:
   - Game buttons disabled for non-hosts (opacity: 0.4, cursor: not-allowed)
   - No hover effects when disabled
   - Clear visual feedback

3. **Messaging**:
   - **Host**: "👑 You're the room host. Select a game to start playing."
   - **Non-host**: " Waiting for [Host Name] to select a game..."
   - **Error**: "⚠️ Only the room host can select a game. Ask [Host] to choose."

4. **Error Handling**:
   - Client-side check before API call
   - Server error caught and displayed with host name
   - Auto-dismiss after 5 seconds

**UX Flow**:
- Non-hosts see disabled games with clear "waiting for host" message
- If they somehow click, they get clear error message
- Host sees active games with confirmation they can select

Prevents confusing 403 errors and clarifies room permissions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 11:59:43 -05:00
semantic-release-bot
0fef1dc9db chore(release): 4.14.3 [skip ci]
## [4.14.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.2...v4.14.3) (2025-10-19)

### Bug Fixes

* **docker:** add qpdf for PDF linearization and validation ([c92ff39](c92ff3971c))
2025-10-19 16:56:12 +00:00
Thomas Hallock
c92ff3971c fix(docker): add qpdf for PDF linearization and validation
The Python flashcard generator requires qpdf for PDF processing.
Without it, the script exits with code 1 even though it prints
a warning saying it will skip linearization.

Added qpdf to Alpine packages to fix PDF generation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 11:55:02 -05:00
semantic-release-bot
50afc3111d chore(release): 4.14.2 [skip ci]
## [4.14.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.1...v4.14.2) (2025-10-19)

### Bug Fixes

* **docker:** add packages/templates for Typst flashcard generation ([1417722](1417722438))
2025-10-19 16:40:37 +00:00
Thomas Hallock
1417722438 fix(docker): add packages/templates for Typst flashcard generation
Typst was failing with "input file not found" error because the templates
directory containing flashcards-input.typ was missing from the Docker image.

Added COPY command to include packages/templates in the production image.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 11:39:29 -05:00
github-actions[bot]
1973c3c5ca 🎨 Update template examples and crop mark gallery
Auto-generated fresh SVG examples and unified gallery from latest templates.
Includes comprehensive crop mark demonstrations with before/after comparisons.

Files updated:
- packages/templates/gallery-unified.html

🤖 Generated with GitHub Actions

Co-Authored-By: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-19 02:34:31 +00:00
semantic-release-bot
0f8e411b92 chore(release): 4.14.1 [skip ci]
## [4.14.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.0...v4.14.1) (2025-10-19)

### Bug Fixes

* **deployment:** pass git info to Docker build for deployment info modal ([4b04e43](4b04e43ff8))
2025-10-19 02:24:27 +00:00
Thomas Hallock
4b04e43ff8 fix(deployment): pass git info to Docker build for deployment info modal
This commit fixes two critical production issues:

1. **Flashcard generator dependencies** - Added all required dependencies to Dockerfile:
   - Typst for PDF generation
   - Python pip and setuptools
   - Python packages (pyyaml, Pillow, imagehash)
   - packages/core directory with generate.py script

2. **Deployment info modal** - Fixed git commit hash display on production:
   - Modified generate-build-info.js to accept env vars as fallback when .git is unavailable
   - Updated Dockerfile to accept GIT_* build arguments
   - Updated GitHub Actions workflow to pass git information during Docker build

The deployment info modal (Ctrl+Shift+I) will now show the correct commit hash,
branch, and build time on production, matching the behavior on dev.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 21:23:15 -05:00
7 changed files with 174 additions and 22 deletions

View File

@@ -57,6 +57,10 @@ jobs:
type=ref,event=pr
type=raw,value=latest,enable={{is_default_branch}}
- name: Get short commit SHA
id: vars
run: echo "short_sha=$(echo ${{ github.sha }} | cut -c1-8)" >> $GITHUB_OUTPUT
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
@@ -64,3 +68,9 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_COMMIT=${{ github.sha }}
GIT_COMMIT_SHORT=${{ steps.vars.outputs.short_sha }}
GIT_BRANCH=${{ github.ref_name }}
GIT_TAG=${{ github.ref_type == 'tag' && github.ref_name || '' }}
GIT_DIRTY=false

View File

@@ -1,3 +1,32 @@
## [4.14.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.3...v4.14.4) (2025-10-19)
### Bug Fixes
* **arcade:** add host-only game selection with clear messaging ([22df1b0](https://github.com/antialias/soroban-abacus-flashcards/commit/22df1b0b661efe69fac1a6bd716531c904757412))
* **arcade:** add host-only game selection with clear messaging ([c0680ca](https://github.com/antialias/soroban-abacus-flashcards/commit/c0680cad0fa26af0933e93a06c50317bf443cc7d))
## [4.14.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.2...v4.14.3) (2025-10-19)
### Bug Fixes
* **docker:** add qpdf for PDF linearization and validation ([c92ff39](https://github.com/antialias/soroban-abacus-flashcards/commit/c92ff3971c853e4e55ccd632ff3ee292fcce8315))
## [4.14.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.1...v4.14.2) (2025-10-19)
### Bug Fixes
* **docker:** add packages/templates for Typst flashcard generation ([1417722](https://github.com/antialias/soroban-abacus-flashcards/commit/14177224380b8c37413123bee344c9b762055a15))
## [4.14.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.0...v4.14.1) (2025-10-19)
### Bug Fixes
* **deployment:** pass git info to Docker build for deployment info modal ([4b04e43](https://github.com/antialias/soroban-abacus-flashcards/commit/4b04e43ff8c9e9f239d7f5e306aab338b535296f))
## [4.14.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.15...v4.14.0) (2025-10-19)

View File

@@ -21,6 +21,21 @@ RUN pnpm install --frozen-lockfile
# Builder stage
FROM base AS builder
# Accept git information as build arguments
ARG GIT_COMMIT
ARG GIT_COMMIT_SHORT
ARG GIT_BRANCH
ARG GIT_TAG
ARG GIT_DIRTY
# Set as environment variables for build scripts
ENV GIT_COMMIT=${GIT_COMMIT}
ENV GIT_COMMIT_SHORT=${GIT_COMMIT_SHORT}
ENV GIT_BRANCH=${GIT_BRANCH}
ENV GIT_TAG=${GIT_TAG}
ENV GIT_DIRTY=${GIT_DIRTY}
COPY . .
# Generate Panda CSS styled-system before building
@@ -33,8 +48,8 @@ RUN turbo build --filter=@soroban/web
FROM node:18-alpine AS runner
WORKDIR /app
# Install Python, pip, build tools for better-sqlite3, and Typst (needed at runtime)
RUN apk add --no-cache python3 py3-pip py3-setuptools make g++ typst
# Install Python, pip, build tools for better-sqlite3, Typst, and qpdf (needed at runtime)
RUN apk add --no-cache python3 py3-pip py3-setuptools make g++ typst qpdf
# Create non-root user
RUN addgroup --system --gid 1001 nodejs
@@ -58,6 +73,9 @@ COPY --from=builder --chown=nextjs:nodejs /app/apps/web/node_modules ./apps/web/
# Copy core package (needed for Python flashcard generation scripts)
COPY --from=builder --chown=nextjs:nodejs /app/packages/core ./packages/core
# Copy templates package (needed for Typst templates)
COPY --from=builder --chown=nextjs:nodejs /app/packages/templates ./packages/templates
# Install Python dependencies for flashcard generation
RUN pip3 install --no-cache-dir --break-system-packages -r packages/core/requirements.txt

View File

@@ -18,11 +18,14 @@ function exec(command) {
}
function getBuildInfo() {
const gitCommit = exec('git rev-parse HEAD')
const gitCommitShort = exec('git rev-parse --short HEAD')
const gitBranch = exec('git rev-parse --abbrev-ref HEAD')
const gitTag = exec('git describe --tags --exact-match 2>/dev/null')
const gitDirty = exec('git diff --quiet || echo "dirty"') === 'dirty'
// Try to get git info from environment variables first (for Docker builds)
// Fall back to git commands (for local development)
const gitCommit = process.env.GIT_COMMIT || exec('git rev-parse HEAD')
const gitCommitShort = process.env.GIT_COMMIT_SHORT || exec('git rev-parse --short HEAD')
const gitBranch = process.env.GIT_BRANCH || exec('git rev-parse --abbrev-ref HEAD')
const gitTag = process.env.GIT_TAG || exec('git describe --tags --exact-match 2>/dev/null')
const gitDirty =
process.env.GIT_DIRTY === 'true' || exec('git diff --quiet || echo "dirty"') === 'dirty'
const packageJson = require('../package.json')

View File

@@ -1,7 +1,9 @@
'use client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { useRoomData, useSetRoomGame } from '@/hooks/useRoomData'
import { useViewerId } from '@/hooks/useViewerId'
import { GAMES_CONFIG } from '@/components/GameSelector'
import type { GameType } from '@/components/GameSelector'
import { PageWithNav } from '@/components/PageWithNav'
@@ -23,7 +25,9 @@ import { getAllGames, getGame, hasGame } from '@/lib/arcade/game-registry'
export default function RoomPage() {
const router = useRouter()
const { roomData, isLoading } = useRoomData()
const { data: viewerId } = useViewerId()
const { mutate: setRoomGame } = useSetRoomGame()
const [permissionError, setPermissionError] = useState<string | null>(null)
// Show loading state
if (isLoading) {
@@ -74,9 +78,27 @@ export default function RoomPage() {
// Show game selection if no game is set
if (!roomData.gameName) {
// Determine if current user is the host
const currentMember = roomData.members.find((m) => m.userId === viewerId)
const isHost = currentMember?.isCreator === true
const hostMember = roomData.members.find((m) => m.isCreator)
const handleGameSelect = (gameType: GameType) => {
console.log('[RoomPage] handleGameSelect called with gameType:', gameType)
// Check if user is host before allowing selection
if (!isHost) {
setPermissionError(
`Only the room host can select a game. Ask ${hostMember?.displayName || 'the host'} to choose.`
)
// Clear error after 5 seconds
setTimeout(() => setPermissionError(null), 5000)
return
}
// Clear any previous errors
setPermissionError(null)
// All games are now in the registry
if (hasGame(gameType)) {
const gameDef = getGame(gameType)
@@ -86,10 +108,21 @@ export default function RoomPage() {
}
console.log('[RoomPage] Selecting registry game:', gameType)
setRoomGame({
roomId: roomData.id,
gameName: gameType,
})
setRoomGame(
{
roomId: roomData.id,
gameName: gameType,
},
{
onError: (error: any) => {
console.error('[RoomPage] Failed to set game:', error)
setPermissionError(
error.message || 'Failed to select game. Only the host can change games.'
)
setTimeout(() => setPermissionError(null), 5000)
},
}
)
return
}
@@ -119,13 +152,70 @@ export default function RoomPage() {
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '8',
mb: '4',
textAlign: 'center',
})}
>
Choose a Game
</h1>
{/* Host info and permission messaging */}
<div
className={css({
maxWidth: '800px',
width: '100%',
mb: '6',
})}
>
{isHost ? (
<div
className={css({
background: 'rgba(34, 197, 94, 0.1)',
border: '1px solid rgba(34, 197, 94, 0.3)',
borderRadius: '8px',
padding: '12px 16px',
color: '#86efac',
fontSize: 'sm',
textAlign: 'center',
})}
>
👑 You're the room host. Select a game to start playing.
</div>
) : (
<div
className={css({
background: 'rgba(234, 179, 8, 0.1)',
border: '1px solid rgba(234, 179, 8, 0.3)',
borderRadius: '8px',
padding: '12px 16px',
color: '#fde047',
fontSize: 'sm',
textAlign: 'center',
})}
>
⏳ Waiting for {hostMember?.displayName || 'the host'} to select a game...
</div>
)}
{/* Permission error message */}
{permissionError && (
<div
className={css({
background: 'rgba(239, 68, 68, 0.1)',
border: '1px solid rgba(239, 68, 68, 0.3)',
borderRadius: '8px',
padding: '12px 16px',
color: '#fca5a5',
fontSize: 'sm',
textAlign: 'center',
mt: '3',
})}
>
⚠️ {permissionError}
</div>
)}
</div>
<div
className={css({
display: 'grid',
@@ -138,21 +228,22 @@ export default function RoomPage() {
{/* Legacy games */}
{Object.entries(GAMES_CONFIG).map(([gameType, config]: [string, any]) => {
const isAvailable = !('available' in config) || config.available !== false
const isDisabled = !isHost || !isAvailable
return (
<button
key={gameType}
onClick={() => handleGameSelect(gameType as GameType)}
disabled={!isAvailable}
disabled={isDisabled}
className={css({
background: config.gradient,
border: '2px solid',
borderColor: config.borderColor || 'blue.200',
borderRadius: '2xl',
padding: '6',
cursor: !isAvailable ? 'not-allowed' : 'pointer',
opacity: !isAvailable ? 0.5 : 1,
cursor: isDisabled ? 'not-allowed' : 'pointer',
opacity: isDisabled ? 0.4 : 1,
transition: 'all 0.3s ease',
_hover: !isAvailable
_hover: isDisabled
? {}
: {
transform: 'translateY(-4px) scale(1.02)',
@@ -193,21 +284,22 @@ export default function RoomPage() {
{/* Registry games */}
{getAllGames().map((gameDef) => {
const isAvailable = gameDef.manifest.available
const isDisabled = !isHost || !isAvailable
return (
<button
key={gameDef.manifest.name}
onClick={() => handleGameSelect(gameDef.manifest.name)}
disabled={!isAvailable}
disabled={isDisabled}
className={css({
background: gameDef.manifest.gradient,
border: '2px solid',
borderColor: gameDef.manifest.borderColor,
borderRadius: '2xl',
padding: '6',
cursor: !isAvailable ? 'not-allowed' : 'pointer',
opacity: !isAvailable ? 0.5 : 1,
cursor: isDisabled ? 'not-allowed' : 'pointer',
opacity: isDisabled ? 0.4 : 1,
transition: 'all 0.3s ease',
_hover: !isAvailable
_hover: isDisabled
? {}
: {
transform: 'translateY(-4px) scale(1.02)',

View File

@@ -1,6 +1,6 @@
{
"name": "soroban-monorepo",
"version": "4.14.0",
"version": "4.14.4",
"private": true,
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
"workspaces": [

View File

@@ -305,7 +305,7 @@
<div class="stats">
<div class="stats-info">
<strong>13</strong> examples rendered
• Generated on 10/12/2025 at 2:30:55 AM
• Generated on 10/19/2025 at 2:34:30 AM
</div>
</div>