From 349537b70e9e84523efd19a7254c823580cea594 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 8 May 2026 04:57:06 +0000 Subject: [PATCH 1/5] ci: add GitHub Actions workflow with split lint/type/build/test jobs Lint, typecheck, build, and test are run as separate jobs across the TS/React, Rust (Tauri), and Python sidecar stacks so failures surface per-stack. Tauri bundle build runs on main push and manual dispatch only; PRs use cargo check to keep CI time bounded. https://claude.ai/code/session_01WSAGRj9p6dadwc1Ld5pbUC --- .github/workflows/ci.yml | 238 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..460b379 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,238 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + NODE_VERSION: "20" + PYTHON_VERSION: "3.12" + RUST_TOOLCHAIN: "stable" + +jobs: + # ---------- Lint ---------- + lint-ts: + name: Lint (TypeScript) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + - run: npm ci + - run: npm run lint + - run: npx prettier --check "src/**/*.{ts,tsx,css,json}" + + lint-rust: + name: Lint (Rust) + runs-on: ubuntu-latest + defaults: + run: + working-directory: src-tauri + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + - name: Install Tauri system deps + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libappindicator3-dev \ + librsvg2-dev \ + patchelf \ + libgtk-3-dev \ + libsoup-3.0-dev \ + libjavascriptcoregtk-4.1-dev + - run: cargo fmt --all -- --check + - run: cargo clippy --all-targets -- -D warnings + + lint-python: + name: Lint (Python) + runs-on: ubuntu-latest + defaults: + run: + working-directory: python + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: python/uv.lock + - run: uv sync --extra dev + - run: uv run ruff check . + - run: uv run black --check . + + # ---------- Type check ---------- + typecheck-ts: + name: Typecheck (TypeScript) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + - run: npm ci + - run: npx tsc -b + + typecheck-python: + name: Typecheck (Python) + runs-on: ubuntu-latest + defaults: + run: + working-directory: python + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: python/uv.lock + - run: uv sync --extra dev + # mortal は torch 依存のため CI では除外 + - run: uv run mypy recognition common + + # ---------- Build ---------- + build-web: + name: Build (Web) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + - run: npm ci + - run: npm run build + - uses: actions/upload-artifact@v4 + with: + name: web-dist + path: dist + retention-days: 7 + + build-rust-check: + name: Build (Rust check) + runs-on: ubuntu-latest + defaults: + run: + working-directory: src-tauri + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + - name: Install Tauri system deps + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libappindicator3-dev \ + librsvg2-dev \ + patchelf \ + libgtk-3-dev \ + libsoup-3.0-dev \ + libjavascriptcoregtk-4.1-dev + - run: cargo check --all-targets + + build-tauri: + name: Build (Tauri ${{ matrix.platform.name }}) + # PR では実行しない (cargo check で代替)。main push か手動のみ。 + if: github.event_name != 'pull_request' + needs: [build-web, build-rust-check] + strategy: + fail-fast: false + matrix: + platform: + - { name: linux, os: ubuntu-latest } + - { name: macos, os: macos-latest } + - { name: windows, os: windows-latest } + runs-on: ${{ matrix.platform.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + - name: Install Tauri system deps (Linux) + if: matrix.platform.name == 'linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libappindicator3-dev \ + librsvg2-dev \ + patchelf \ + libgtk-3-dev \ + libsoup-3.0-dev \ + libjavascriptcoregtk-4.1-dev + - run: npm ci + - run: npm run tauri:build + + # ---------- Test ---------- + test-ts: + name: Test (TypeScript / Vitest) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + - run: npm ci + - run: npm test + + test-rust: + name: Test (Rust / cargo test) + runs-on: ubuntu-latest + defaults: + run: + working-directory: src-tauri + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + - name: Install Tauri system deps + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libappindicator3-dev \ + librsvg2-dev \ + patchelf \ + libgtk-3-dev \ + libsoup-3.0-dev \ + libjavascriptcoregtk-4.1-dev + - run: cargo test --all-targets + + test-python: + name: Test (Python / pytest) + runs-on: ubuntu-latest + defaults: + run: + working-directory: python + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: python/uv.lock + - run: uv sync --extra dev + - run: uv run pytest -q From 7948ba4ab2565d9f3ca290055371f5cbf613a6df Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 8 May 2026 05:32:24 +0000 Subject: [PATCH 2/5] fix: resolve all CI failures from initial run - lint-rust: apply cargo fmt across src-tauri - lint-python: replace timezone.utc with datetime.UTC alias (UP017) - typecheck/build: - move vite.config.ts solely under tsconfig.node.json (composite project) - drop async wrapper and unused @ts-expect-error in vite.config.ts - add /// for the test field - lint-ts: migrate ESLint to v9 flat config (eslint.config.js) - add @eslint/js, globals, typescript-eslint dev deps - remove legacy .eslintrc.cjs - prettier: format src/** to satisfy the new prettier --check step - gitignore: ignore *.tsbuildinfo https://claude.ai/code/session_01WSAGRj9p6dadwc1Ld5pbUC --- .eslintrc.cjs | 22 ------- .gitignore | 1 + eslint.config.js | 50 ++++++++++++++ package-lock.json | 75 ++++++++++++++------- package.json | 3 + python/mortal/main.py | 4 +- src-tauri/src/commands.rs | 17 ++--- src-tauri/src/lib.rs | 5 +- src/App.tsx | 17 +---- src/components/CandidateList.tsx | 20 ++---- src/components/DangerSafeBlock.tsx | 6 +- src/components/ErrorBody.tsx | 8 +-- src/components/HandRow.tsx | 21 ++---- src/components/HeroLayout.tsx | 8 +-- src/components/IdleBody.tsx | 4 +- src/components/MonitorButton.tsx | 5 +- src/components/PrimaryGlyph.tsx | 7 +- src/components/ReasonBlock.tsx | 4 +- src/components/StatusBar.tsx | 4 +- src/components/Tile.tsx | 31 ++------- src/index.css | 101 +++++++++++++++++------------ src/lib/scenarios.ts | 53 ++------------- src/lib/tauriCommands.ts | 7 +- src/screens/MainScreen.tsx | 45 +++---------- src/screens/SettingsScreen.tsx | 85 +++++------------------- src/state/appState.ts | 11 +--- tsconfig.json | 2 +- tsconfig.node.json | 9 ++- vite.config.ts | 6 +- 29 files changed, 242 insertions(+), 389 deletions(-) delete mode 100644 .eslintrc.cjs create mode 100644 eslint.config.js diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index ad3812f..0000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,22 +0,0 @@ -/* eslint-env node */ -module.exports = { - root: true, - env: { browser: true, es2022: true, node: true }, - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react-hooks/recommended", - ], - ignorePatterns: ["dist", "src-tauri/target", "node_modules", ".eslintrc.cjs"], - parser: "@typescript-eslint/parser", - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - project: "./tsconfig.json", - }, - plugins: ["react-refresh", "@typescript-eslint"], - rules: { - "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], - "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], - }, -}; diff --git a/.gitignore b/.gitignore index a1e2a88..da6b546 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ dist-ssr/ .parcel-cache/ .turbo/ *.local +*.tsbuildinfo # Testing coverage/ diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..a6ec655 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,50 @@ +import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; + +export default tseslint.config( + { + ignores: [ + "dist", + "src-tauri/target", + "node_modules", + "vite.config.ts", + "eslint.config.js", + "scripts", + ], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["src/**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + globals: { + ...globals.browser, + ...globals.node, + }, + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: import.meta.dirname, + }, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + "@typescript-eslint/no-unused-vars": [ + "warn", + { argsIgnorePattern: "^_" }, + ], + }, + }, +); diff --git a/package-lock.json b/package-lock.json index 7054090..d617289 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "tailwind-merge": "^2.5.4" }, "devDependencies": { + "@eslint/js": "^9.39.4", "@tailwindcss/vite": "^4.0.0", "@tauri-apps/cli": "^2.1.0", "@testing-library/jest-dom": "^6.6.3", @@ -35,10 +36,12 @@ "eslint": "^9.14.0", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", + "globals": "^15.15.0", "jsdom": "^25.0.1", "prettier": "^3.3.3", "tailwindcss": "^4.0.0", "typescript": "^5.6.3", + "typescript-eslint": "^8.59.2", "vite": "^5.4.10", "vitest": "^2.1.4" } @@ -102,7 +105,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -472,7 +474,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -496,7 +497,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1035,6 +1035,19 @@ "concat-map": "0.0.1" } }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/eslintrc/node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2217,7 +2230,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -2284,7 +2298,6 @@ "integrity": "sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2302,7 +2315,6 @@ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2314,7 +2326,6 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -2354,7 +2365,6 @@ "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", @@ -2693,7 +2703,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2744,6 +2753,7 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -2854,7 +2864,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -3177,7 +3186,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dunder-proto": { "version": "1.0.1", @@ -3352,7 +3362,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3806,9 +3815,9 @@ } }, "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", "dev": true, "license": "MIT", "engines": { @@ -4063,7 +4072,6 @@ "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.1.0", "data-urls": "^5.0.0", @@ -4498,6 +4506,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -4744,7 +4753,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4813,6 +4821,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -4828,6 +4837,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -4850,7 +4860,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -4863,7 +4872,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -4877,7 +4885,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-refresh": { "version": "0.17.0", @@ -5284,7 +5293,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5293,6 +5301,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.2.tgz", + "integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.2", + "@typescript-eslint/parser": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/utils": "8.59.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -5347,7 +5379,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/package.json b/package.json index f6f95f7..60003a0 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "tailwind-merge": "^2.5.4" }, "devDependencies": { + "@eslint/js": "^9.39.4", "@tailwindcss/vite": "^4.0.0", "@tauri-apps/cli": "^2.1.0", "@testing-library/jest-dom": "^6.6.3", @@ -44,10 +45,12 @@ "eslint": "^9.14.0", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", + "globals": "^15.15.0", "jsdom": "^25.0.1", "prettier": "^3.3.3", "tailwindcss": "^4.0.0", "typescript": "^5.6.3", + "typescript-eslint": "^8.59.2", "vite": "^5.4.10", "vitest": "^2.1.4" } diff --git a/python/mortal/main.py b/python/mortal/main.py index 459bb6d..9731b82 100644 --- a/python/mortal/main.py +++ b/python/mortal/main.py @@ -12,7 +12,7 @@ import argparse import sys -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any from common import read_request, setup_stderr_logging, write_response @@ -30,7 +30,7 @@ def stub_inference(_tenhou_json: dict[str, Any]) -> dict[str, Any]: return { "recommended": candidates[0], "candidates": candidates, - "timestamp": datetime.now(timezone.utc).isoformat(), + "timestamp": datetime.now(UTC).isoformat(), } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index e44f1b4..b2fdbd1 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -20,9 +20,7 @@ pub async fn list_capture_windows() -> Result, String> { #[tauri::command] pub async fn load_settings(app: AppHandle) -> Result, String> { - let store = app - .store(SETTINGS_STORE_FILE) - .map_err(|e| e.to_string())?; + let store = app.store(SETTINGS_STORE_FILE).map_err(|e| e.to_string())?; let value = store.get(SETTINGS_KEY); if let Some(v) = value { let parsed: AppSettings = @@ -35,9 +33,7 @@ pub async fn load_settings(app: AppHandle) -> Result, String #[tauri::command] pub async fn save_settings(app: AppHandle, settings: AppSettings) -> Result<(), String> { - let store = app - .store(SETTINGS_STORE_FILE) - .map_err(|e| e.to_string())?; + let store = app.store(SETTINGS_STORE_FILE).map_err(|e| e.to_string())?; store.set( SETTINGS_KEY, serde_json::to_value(&settings).map_err(|e| e.to_string())?, @@ -47,10 +43,7 @@ pub async fn save_settings(app: AppHandle, settings: AppSettings) -> Result<(), } #[tauri::command] -pub async fn start_monitoring( - app: AppHandle, - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn start_monitoring(app: AppHandle, state: State<'_, AppState>) -> Result<(), String> { // 既に動作中なら何もしない { let guard = state.monitor_handle.lock().unwrap(); @@ -60,9 +53,7 @@ pub async fn start_monitoring( } // 設定からキャプチャ対象を取得 - let settings = load_settings(app.clone()) - .await? - .unwrap_or_default(); + let settings = load_settings(app.clone()).await?.unwrap_or_default(); let target = settings .capture_target_window_id .clone() diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8bef3f2..c102048 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -36,10 +36,7 @@ pub fn run() { .plugin(tauri_plugin_store::Builder::new().build()) .plugin( tauri_plugin_sql::Builder::default() - .add_migrations( - "sqlite:jantama-ai.db", - db_migrations(), - ) + .add_migrations("sqlite:jantama-ai.db", db_migrations()) .build(), ) .setup(|app| { diff --git a/src/App.tsx b/src/App.tsx index f85c810..d9a228d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,14 +8,7 @@ import type { GameBoardSummary, InferenceResult } from "@/types"; type Screen = "main" | "settings"; function App() { - const { - state, - setPhase, - setSettings, - setMonitoring, - setInference, - setBoard, - } = useAppState(); + const { state, setPhase, setSettings, setMonitoring, setInference, setBoard } = useAppState(); const [screen, setScreen] = useState("main"); // 起動時に保存済み設定を読み込む @@ -28,9 +21,7 @@ function App() { if (loaded) { setSettings(loaded); setPhase( - loaded.capture_target_window_id && loaded.mortal_model_path - ? "idle" - : "uninitialized", + loaded.capture_target_window_id && loaded.mortal_model_path ? "idle" : "uninitialized", ); } else { setPhase("uninitialized"); @@ -81,9 +72,7 @@ function App() { onSaved={(next) => { setSettings(next); setPhase( - next.capture_target_window_id && next.mortal_model_path - ? "idle" - : "uninitialized", + next.capture_target_window_id && next.mortal_model_path ? "idle" : "uninitialized", ); setScreen("main"); }} diff --git a/src/components/CandidateList.tsx b/src/components/CandidateList.tsx index a4f7de4..c518c49 100644 --- a/src/components/CandidateList.tsx +++ b/src/components/CandidateList.tsx @@ -8,10 +8,7 @@ interface CandidateListProps { startRank?: number; } -export function CandidateList({ - candidates, - startRank = 2, -}: CandidateListProps) { +export function CandidateList({ candidates, startRank = 2 }: CandidateListProps) { if (candidates.length === 0) return null; return (
@@ -31,15 +28,8 @@ export function CandidateList({ ); } -function CandidateRow({ - candidate, - rank, -}: { - candidate: RecommendationCandidate; - rank: number; -}) { - const display = - candidate.tile ?? candidate.action_label ?? ACTION_LABEL[candidate.action_type]; +function CandidateRow({ candidate, rank }: { candidate: RecommendationCandidate; rank: number }) { + const display = candidate.tile ?? candidate.action_label ?? ACTION_LABEL[candidate.action_type]; const tileCode = candidate.tile && isTileCode(candidate.tile) ? candidate.tile : null; const ev = candidate.expected_value; return ( @@ -51,9 +41,7 @@ function CandidateRow({ {tileCode ? ( ) : ( - - {display} - + {display} )}
diff --git a/src/components/DangerSafeBlock.tsx b/src/components/DangerSafeBlock.tsx index ef84f8f..957481c 100644 --- a/src/components/DangerSafeBlock.tsx +++ b/src/components/DangerSafeBlock.tsx @@ -12,11 +12,7 @@ export function DangerSafeBlock({ danger, safe }: DangerSafeBlockProps) { if (!hasDanger && !hasSafe) return null; return ( -
+
{hasDanger && (
-
- {title} -
-

- {message} -

+
{title}
+

{message}

{onRetry && (
{hand.slice(0, 13).map((t, i) => ( - + ))} {hand.length === 14 && ( <>
- + )}
diff --git a/src/components/HeroLayout.tsx b/src/components/HeroLayout.tsx index 0a01efe..4462dad 100644 --- a/src/components/HeroLayout.tsx +++ b/src/components/HeroLayout.tsx @@ -48,9 +48,7 @@ export function HeroLayout({ inference }: HeroLayoutProps) { {inference.primary_label ?? actionLabel(recommended)}
- - EV - + EV {evScore} @@ -59,9 +57,7 @@ export function HeroLayout({ inference }: HeroLayoutProps) {
確信度 - - {Math.round(probability * 100)}% - + {Math.round(probability * 100)}%
diff --git a/src/components/IdleBody.tsx b/src/components/IdleBody.tsx index d0f5964..c60b0a5 100644 --- a/src/components/IdleBody.tsx +++ b/src/components/IdleBody.tsx @@ -18,9 +18,7 @@ export function IdleBody() {
-
- 対局を待機中 -
+
対局を待機中
雀魂のウィンドウを監視しています。自分の手番が来ると、ここに推奨アクションを表示します。
diff --git a/src/components/MonitorButton.tsx b/src/components/MonitorButton.tsx index a5bb049..df98ad5 100644 --- a/src/components/MonitorButton.tsx +++ b/src/components/MonitorButton.tsx @@ -18,10 +18,7 @@ export function MonitorButton({ on, disabled, onClick }: MonitorButtonProps) { )} style={!on ? { background: "var(--gradient-acial)" } : undefined} > - + {on ? "監視中 — 停止する" : "監視を開始する"} ); diff --git a/src/components/PrimaryGlyph.tsx b/src/components/PrimaryGlyph.tsx index 348a289..2c82795 100644 --- a/src/components/PrimaryGlyph.tsx +++ b/src/components/PrimaryGlyph.tsx @@ -32,12 +32,9 @@ function VerbCard({ label, size = "xl" }: { label: string; size: TileSize }) { style={{ width: dims, height: dims * 1.3, - background: isGradient - ? "linear-gradient(135deg, #0432FF 0%, #FF2600 100%)" - : "#0F0F1E", + background: isGradient ? "linear-gradient(135deg, #0432FF 0%, #FF2600 100%)" : "#0F0F1E", fontSize: dims * 0.32, - boxShadow: - "0 12px 40px rgba(15,15,30,0.18), 0 2px 6px rgba(15,15,30,0.08)", + boxShadow: "0 12px 40px rgba(15,15,30,0.18), 0 2px 6px rgba(15,15,30,0.08)", }} > {label} diff --git a/src/components/ReasonBlock.tsx b/src/components/ReasonBlock.tsx index 243ffe2..df1fe0e 100644 --- a/src/components/ReasonBlock.tsx +++ b/src/components/ReasonBlock.tsx @@ -17,9 +17,7 @@ export function ReasonBlock({ reason }: ReasonBlockProps) { Reasoning
-

- {reason} -

+

{reason}

); } diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx index 13e3399..4dbe82f 100644 --- a/src/components/StatusBar.tsx +++ b/src/components/StatusBar.tsx @@ -22,9 +22,7 @@ export function StatusBar({ monitoring, onOpenSettings }: StatusBarProps) {
tama - - AI - + AI
diff --git a/src/components/Tile.tsx b/src/components/Tile.tsx index 49970c0..55e4fbf 100644 --- a/src/components/Tile.tsx +++ b/src/components/Tile.tsx @@ -116,13 +116,7 @@ function PinDots({ n, red }: { n: number; red?: boolean }) { return ( <> {pts.map(([x, y], i) => ( - + ))} ); @@ -323,30 +317,13 @@ export function Tile({ : "drop-shadow(0 1px 1px rgba(15,15,30,0.10))", }} > - + - + - + diff --git a/src/index.css b/src/index.css index 8f7c0b5..84fe581 100644 --- a/src/index.css +++ b/src/index.css @@ -1,4 +1,4 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&family=JetBrains+Mono:wght@400;500&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&family=JetBrains+Mono:wght@400;500&display=swap"); @import "tailwindcss"; /* ========================================================= @@ -8,8 +8,9 @@ /* Local Noto Sans JP (variable, brand-supplied) */ @font-face { font-family: "Noto Sans JP"; - src: url("/fonts/NotoSansJP-VariableFont_wght.ttf") format("truetype-variations"), - url("/fonts/NotoSansJP-VariableFont_wght.ttf") format("truetype"); + src: + url("/fonts/NotoSansJP-VariableFont_wght.ttf") format("truetype-variations"), + url("/fonts/NotoSansJP-VariableFont_wght.ttf") format("truetype"); font-weight: 100 900; font-style: normal; font-display: swap; @@ -22,45 +23,45 @@ --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; /* Brand */ - --color-acial-blue: #0432FF; - --color-acial-red: #FF2600; + --color-acial-blue: #0432ff; + --color-acial-red: #ff2600; /* Brand color steps */ - --color-blue-50: #EEF1FF; - --color-blue-100: #D7DEFF; - --color-blue-300: #6E7FFF; - --color-blue-500: #0432FF; - --color-blue-700: #0024C7; + --color-blue-50: #eef1ff; + --color-blue-100: #d7deff; + --color-blue-300: #6e7fff; + --color-blue-500: #0432ff; + --color-blue-700: #0024c7; --color-blue-900: #001280; - --color-red-50: #FFEEE9; - --color-red-100: #FFD3C7; - --color-red-300: #FF7A5C; - --color-red-500: #FF2600; - --color-red-700: #C71B00; - --color-red-900: #800F00; + --color-red-50: #ffeee9; + --color-red-100: #ffd3c7; + --color-red-300: #ff7a5c; + --color-red-500: #ff2600; + --color-red-700: #c71b00; + --color-red-900: #800f00; /* Neutrals (warm, slightly off-white) */ - --color-ink-900: #0F0F1E; - --color-ink-800: #1F1F2E; - --color-ink-700: #34344A; + --color-ink-900: #0f0f1e; + --color-ink-800: #1f1f2e; + --color-ink-700: #34344a; --color-ink-600: #545468; - --color-ink-500: #7A7A8C; - --color-ink-400: #A0A0AE; - --color-ink-300: #C8C7CF; - --color-ink-200: #E6E5DF; - --color-ink-100: #F2F1EC; - --color-ink-50: #FAFAF7; + --color-ink-500: #7a7a8c; + --color-ink-400: #a0a0ae; + --color-ink-300: #c8c7cf; + --color-ink-200: #e6e5df; + --color-ink-100: #f2f1ec; + --color-ink-50: #fafaf7; /* Semantic */ - --color-success: #138A4F; - --color-success-bg: #E5F6EC; - --color-warning: #B8740A; - --color-warning-bg: #FFF3DC; - --color-danger: #C71B00; - --color-danger-bg: #FFEEE9; - --color-info: #0024C7; - --color-info-bg: #EEF1FF; + --color-success: #138a4f; + --color-success-bg: #e5f6ec; + --color-warning: #b8740a; + --color-warning-bg: #fff3dc; + --color-danger: #c71b00; + --color-danger-bg: #ffeee9; + --color-info: #0024c7; + --color-info-bg: #eef1ff; /* Radii */ --radius-1: 6px; @@ -72,10 +73,15 @@ /* ── Plain CSS variables (used by inline styles & gradients) ─ */ :root { /* Brand gradients (cannot live inside @theme without breaking tailwind utilities) */ - --gradient-acial: linear-gradient(135deg, #0432FF 0%, #FF2600 100%); - --gradient-acial-soft: linear-gradient(135deg, rgba(4, 50, 255, 0.10) 0%, rgba(255, 38, 0, 0.10) 100%); - --gradient-acial-halo: radial-gradient(60% 60% at 30% 40%, rgba(4, 50, 255, 0.18) 0%, transparent 60%), - radial-gradient(50% 50% at 80% 70%, rgba(255, 38, 0, 0.14) 0%, transparent 60%); + --gradient-acial: linear-gradient(135deg, #0432ff 0%, #ff2600 100%); + --gradient-acial-soft: linear-gradient( + 135deg, + rgba(4, 50, 255, 0.1) 0%, + rgba(255, 38, 0, 0.1) 100% + ); + --gradient-acial-halo: + radial-gradient(60% 60% at 30% 40%, rgba(4, 50, 255, 0.18) 0%, transparent 60%), + radial-gradient(50% 50% at 80% 70%, rgba(255, 38, 0, 0.14) 0%, transparent 60%); /* Elevation */ --shadow-1: 0 1px 2px rgba(15, 15, 30, 0.04), 0 0 0 1px rgba(15, 15, 30, 0.03); @@ -89,7 +95,9 @@ } /* ── Base ──────────────────────────────────────────────── */ -html, body, #root { +html, +body, +#root { height: 100%; margin: 0; padding: 0; @@ -135,13 +143,22 @@ body { /* ── Animations ────────────────────────────────────────── */ @keyframes jt-pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.4; } + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.4; + } } @keyframes jt-spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } } .animate-jt-pulse { diff --git a/src/lib/scenarios.ts b/src/lib/scenarios.ts index f23ff3f..b98ed2c 100644 --- a/src/lib/scenarios.ts +++ b/src/lib/scenarios.ts @@ -4,10 +4,7 @@ * `runStubInference` のフォールバックなどで利用する。 */ -import type { - GameBoardSummary, - InferenceResult, -} from "@/types"; +import type { GameBoardSummary, InferenceResult } from "@/types"; export type ScenarioKey = "dahai" | "riichi" | "fuuro" | "agari"; @@ -17,51 +14,9 @@ interface ScenarioFixture { } // Hand fixtures — same physical hands the design canvas uses. -const HAND_TENPAI = [ - "1m", - "2m", - "3m", - "4m", - "5m", - "6m", - "7p", - "8p", - "9p", - "1z", - "2z", - "3z", - "5m", -]; -const HAND_MENZEN = [ - "2m", - "3m", - "4m", - "6m", - "7m", - "8m", - "3p", - "4p", - "5p", - "7s", - "8s", - "9s", - "7z", -]; -const HAND_AGARI = [ - "1m", - "2m", - "3m", - "4m", - "5m", - "6m", - "7p", - "8p", - "9p", - "1z", - "1z", - "1z", - "5p", -]; +const HAND_TENPAI = ["1m", "2m", "3m", "4m", "5m", "6m", "7p", "8p", "9p", "1z", "2z", "3z", "5m"]; +const HAND_MENZEN = ["2m", "3m", "4m", "6m", "7m", "8m", "3p", "4p", "5p", "7s", "8s", "9s", "7z"]; +const HAND_AGARI = ["1m", "2m", "3m", "4m", "5m", "6m", "7p", "8p", "9p", "1z", "1z", "1z", "5p"]; const COMMON_BOARD: GameBoardSummary = { hand: HAND_TENPAI, diff --git a/src/lib/tauriCommands.ts b/src/lib/tauriCommands.ts index 0847ced..97da228 100644 --- a/src/lib/tauriCommands.ts +++ b/src/lib/tauriCommands.ts @@ -6,12 +6,7 @@ */ import { invoke } from "@tauri-apps/api/core"; -import type { - AppSettings, - CaptureWindow, - GameBoardSummary, - InferenceResult, -} from "@/types"; +import type { AppSettings, CaptureWindow, GameBoardSummary, InferenceResult } from "@/types"; import { nextStubScenario } from "@/lib/scenarios"; function isTauri(): boolean { diff --git a/src/screens/MainScreen.tsx b/src/screens/MainScreen.tsx index 5217d29..94a285f 100644 --- a/src/screens/MainScreen.tsx +++ b/src/screens/MainScreen.tsx @@ -8,11 +8,7 @@ import { IdleBody } from "@/components/IdleBody"; import { MonitorButton } from "@/components/MonitorButton"; import { ReasonBlock } from "@/components/ReasonBlock"; import { StatusBar } from "@/components/StatusBar"; -import { - runStubInference, - startMonitoring, - stopMonitoring, -} from "@/lib/tauriCommands"; +import { runStubInference, startMonitoring, stopMonitoring } from "@/lib/tauriCommands"; import type { AppState } from "@/state/appState"; import type { GameBoardSummary, InferenceResult } from "@/types"; @@ -20,10 +16,7 @@ interface MainScreenProps { state: AppState; onOpenSettings: () => void; onMonitoringChange: (watching: boolean) => void; - onInferenceUpdate: ( - inference: InferenceResult, - board: GameBoardSummary | null, - ) => void; + onInferenceUpdate: (inference: InferenceResult, board: GameBoardSummary | null) => void; } export function MainScreen({ @@ -69,23 +62,14 @@ export function MainScreen({
- - + +
- +
{state.monitoring.watching && state.inference && state.board && ( @@ -103,13 +87,7 @@ export function MainScreen({ ); } -function MainBody({ - state, - onOpenSettings, -}: { - state: AppState; - onOpenSettings: () => void; -}) { +function MainBody({ state, onOpenSettings }: { state: AppState; onOpenSettings: () => void }) { if (state.phase === "uninitialized") { return ( - {state.settings.show_llm_reason && ( - - )} + {state.settings.show_llm_reason && } {state.settings.show_danger_safe && ( - + )}
); diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 549e678..124a5bc 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -1,10 +1,6 @@ import { useEffect, useState } from "react"; import { ChevronDown, ChevronLeft } from "lucide-react"; -import { - type AppSettings, - type CaptureWindow, - type InferenceBackend, -} from "@/types"; +import { type AppSettings, type CaptureWindow, type InferenceBackend } from "@/types"; import { listCaptureWindows, saveSettings } from "@/lib/tauriCommands"; import { cn } from "@/lib/utils"; @@ -14,11 +10,7 @@ interface SettingsScreenProps { onSaved: (next: AppSettings) => void; } -export function SettingsScreen({ - initialSettings, - onBack, - onSaved, -}: SettingsScreenProps) { +export function SettingsScreen({ initialSettings, onBack, onSaved }: SettingsScreenProps) { const [settings, setSettings] = useState(initialSettings); const [windows, setWindows] = useState([]); const [loading, setLoading] = useState(true); @@ -46,19 +38,14 @@ export function SettingsScreen({ }; }, []); - const selectedWindow = windows.find( - (w) => w.id === settings.capture_target_window_id, - ); + const selectedWindow = windows.find((w) => w.id === settings.capture_target_window_id); const handlePickWindow = () => { // 画面側で簡易的にローテートさせる (デザイン版はネイティブのドロップダウン代替) if (windows.length === 0) return; - const currentIndex = windows.findIndex( - (w) => w.id === settings.capture_target_window_id, - ); + const currentIndex = windows.findIndex((w) => w.id === settings.capture_target_window_id); // 未選択 (-1) の場合は先頭に。選択済みなら次の要素にローテート。 - const nextIndex = - currentIndex === -1 ? 0 : (currentIndex + 1) % windows.length; + const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % windows.length; const next = windows[nextIndex]; setSettings((s) => ({ ...s, @@ -113,8 +100,7 @@ export function SettingsScreen({
{/* ヘッダー */} @@ -127,9 +113,7 @@ export function SettingsScreen({ > -

- 設定 -

+

設定

{/* 中央 */} @@ -137,11 +121,7 @@ export function SettingsScreen({ @@ -187,9 +167,7 @@ export function SettingsScreen({ - setSettings((s) => ({ ...s, show_danger_safe: v })) - } + onChange={(v) => setSettings((s) => ({ ...s, show_danger_safe: v }))} /> S-02 — 他家リーチ時に常時表示 @@ -266,13 +244,7 @@ export function SettingsScreen({ ); } -function SettingGroup({ - label, - children, -}: { - label: string; - children: React.ReactNode; -}) { +function SettingGroup({ label, children }: { label: string; children: React.ReactNode }) { return (
@@ -283,13 +255,7 @@ function SettingGroup({ ); } -function SelectField({ - value, - onClick, -}: { - value: string; - onClick?: () => void; -}) { +function SelectField({ value, onClick }: { value: string; onClick?: () => void }) { return (