diff --git a/AGENTS.md b/AGENTS.md index 41b7c46..c67779b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,31 +1,71 @@ -# AnthoLume - Agent Context +# AnthoLume Agent Guide -## Critical Rules +## 1) Working Style -### Generated Files -- **NEVER edit generated files directly** - Always edit the source and regenerate -- Go backend API: Edit `api/v1/openapi.yaml` then run: - - `go generate ./api/v1/generate.go` - - `cd frontend && bun run generate:api` -- Examples of generated files: - - `api/v1/api.gen.go` - - `frontend/src/generated/**/*.ts` +- Keep changes targeted. +- Do not refactor broadly unless the task requires it. +- Validate only what is relevant to the change when practical. +- If a fix will require substantial refactoring or wide-reaching changes, stop and ask first. -### Database Access -- **NEVER write ad-hoc SQL** - Only use SQLC queries from `database/query.sql` -- Define queries in `database/query.sql` and regenerate via `sqlc generate` +## 2) Hard Rules -### Error Handling -- Use `fmt.Errorf("message: %w", err)` for wrapping errors -- Do NOT use `github.com/pkg/errors` +- Never edit generated files directly. +- Never write ad-hoc SQL. +- For Go error wrapping, use `fmt.Errorf("message: %w", err)`. +- Do not use `github.com/pkg/errors`. -## Frontend -- **Package manager**: bun (not npm) -- **Icons**: Use custom icon components in `src/icons/` (not external icon libraries) -- **Lint**: `cd frontend && bun run lint` (and `lint:fix`) -- **Format**: `cd frontend && bun run format` (and `format:fix`) -- **Generate API client**: `cd frontend && bun run generate:api` +## 3) Generated Code -## Regeneration -- Go backend: `go generate ./api/v1/generate.go` -- TS client: `cd frontend && bun run generate:api` +### OpenAPI +Edit: +- `api/v1/openapi.yaml` + +Regenerate: +- `go generate ./api/v1/generate.go` +- `cd frontend && bun run generate:api` + +Examples of generated files: +- `api/v1/api.gen.go` +- `frontend/src/generated/**/*.ts` + +### SQLC +Edit: +- `database/query.sql` + +Regenerate: +- `sqlc generate` + +## 4) Backend / Assets + +### Common commands +- Dev server: `make dev` +- Direct dev run: `CONFIG_PATH=./data DATA_PATH=./data REGISTRATION_ENABLED=true go run main.go serve` +- Tests: `make tests` +- Tailwind asset build: `make build_tailwind` + +### Notes +- The Go server embeds `templates/*` and `assets/*`. +- Root Tailwind output is built to `assets/style.css`. +- Be mindful of whether a change affects the embedded server-rendered app, the React frontend, or both. + +## 5) Frontend + +For frontend-specific implementation notes and commands, also read: +- `frontend/AGENTS.md` + +## 6) Regeneration Summary + +- Go API: `go generate ./api/v1/generate.go` +- Frontend API client: `cd frontend && bun run generate:api` +- SQLC: `sqlc generate` + +## 7) Updating This File + +After completing a task, update this `AGENTS.md` if you learned something general that would help future agents. + +Rules for updates: +- Add only repository-wide guidance. +- Do not add one-off task history. +- Keep updates short, concrete, and organized. +- Place new guidance in the most relevant section. +- If the new information would help future agents avoid repeated mistakes, add it proactively. diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md new file mode 100644 index 0000000..546a28c --- /dev/null +++ b/frontend/AGENTS.md @@ -0,0 +1,69 @@ +# AnthoLume Frontend Agent Guide + +Read this file for work in `frontend/`. +Also follow the repository root guide at `../AGENTS.md`. + +## 1) Stack + +- Package manager: `bun` +- Framework: React + Vite +- Data fetching: React Query +- API generation: Orval +- Linting: ESLint + Tailwind plugin +- Formatting: Prettier + +## 2) Conventions + +- Use local icon components from `src/icons/`. +- Do not add external icon libraries. +- Prefer generated types from `src/generated/model/` over `any`. +- Avoid custom class names in JSX `className` values unless the Tailwind lint config already allows them. + +## 3) Generated API client + +- Do not edit `src/generated/**` directly. +- Edit `../api/v1/openapi.yaml` and regenerate instead. +- Regenerate with: `bun run generate:api` + +### Important behavior + +- The generated client returns `{ data, status, headers }` for both success and error responses. +- Do not assume non-2xx responses throw. +- Check `response.status` and response shape before treating a request as successful. + +## 4) Auth / Query State + +- When changing auth flows, account for React Query cache state. +- Pay special attention to `/api/v1/auth/me`. +- A local auth state update may not be enough if cached query data still reflects a previous auth state. + +## 5) Commands + +- Lint: `bun run lint` +- Typecheck: `bun run typecheck` +- Lint fix: `bun run lint:fix` +- Format check: `bun run format` +- Format fix: `bun run format:fix` +- Build: `bun run build` +- Generate API client: `bun run generate:api` + +## 6) Validation Notes + +- ESLint ignores `src/generated/**`. +- Frontend unit tests use Vitest and live alongside source as `src/**/*.test.ts(x)`. +- `bun run lint` includes test files but does not typecheck. +- Use `bun run typecheck` to run TypeScript validation for app code and colocated tests without a full production build. +- Run frontend tests with `bun run test`. +- `bun run build` still runs `tsc && vite build`, so unrelated TypeScript issues elsewhere in `src/` can fail the build. +- When possible, validate changed files directly before escalating to full-project fixes. + +## 7) Updating This File + +After completing a frontend task, update this file if you learned something general that would help future frontend agents. + +Rules for updates: + +- Add only frontend-wide guidance. +- Do not record one-off task history. +- Keep updates concise and action-oriented. +- Prefer notes that prevent repeated mistakes. diff --git a/frontend/bun.lock b/frontend/bun.lock index 487be23..093fe4f 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -9,7 +9,6 @@ "ajv": "^8.18.0", "axios": "^1.13.6", "clsx": "^2.1.1", - "lucide-react": "^0.577.0", "orval": "8.5.3", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -35,6 +34,7 @@ "tailwindcss": "^3.4.17", "typescript": "~5.6.2", "vite": "^6.0.5", + "vitest": "^4.1.0", }, }, }, @@ -277,6 +277,8 @@ "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="], "@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="], @@ -289,6 +291,10 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], @@ -325,6 +331,20 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], + "@vitest/expect": ["@vitest/expect@4.1.0", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.0", "@vitest/utils": "4.1.0", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" } }, "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.0", "", { "dependencies": { "@vitest/spy": "4.1.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.0", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A=="], + + "@vitest/runner": ["@vitest/runner@4.1.0", "", { "dependencies": { "@vitest/utils": "4.1.0", "pathe": "^2.0.3" } }, "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.0", "", { "dependencies": { "@vitest/pretty-format": "4.1.0", "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg=="], + + "@vitest/spy": ["@vitest/spy@4.1.0", "", {}, "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw=="], + + "@vitest/utils": ["@vitest/utils@4.1.0", "", { "dependencies": { "@vitest/pretty-format": "4.1.0", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" } }, "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw=="], + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], @@ -363,6 +383,8 @@ "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], @@ -397,6 +419,8 @@ "caniuse-lite": ["caniuse-lite@1.0.30001779", "", {}, "sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], @@ -463,6 +487,8 @@ "es-iterator-helpers": ["es-iterator-helpers@1.3.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0", "safe-array-concat": "^1.1.3" } }, "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ=="], + "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], @@ -501,10 +527,14 @@ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], @@ -707,10 +737,10 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "lucide-react": ["lucide-react@0.577.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="], - "lunr": ["lunr@2.3.9", "", {}, "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -759,6 +789,8 @@ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "orval": ["orval@8.5.3", "", { "dependencies": { "@commander-js/extra-typings": "^14.0.0", "@orval/angular": "8.5.3", "@orval/axios": "8.5.3", "@orval/core": "8.5.3", "@orval/fetch": "8.5.3", "@orval/hono": "8.5.3", "@orval/mcp": "8.5.3", "@orval/mock": "8.5.3", "@orval/query": "8.5.3", "@orval/solid-start": "8.5.3", "@orval/swr": "8.5.3", "@orval/zod": "8.5.3", "@scalar/json-magic": "^0.11.5", "@scalar/openapi-parser": "^0.24.13", "@scalar/openapi-types": "0.5.3", "chokidar": "^5.0.0", "commander": "^14.0.2", "enquirer": "^2.4.1", "execa": "^9.6.1", "find-up": "8.0.0", "fs-extra": "^11.3.2", "jiti": "^2.6.1", "js-yaml": "4.1.1", "remeda": "^2.33.6", "string-argv": "^0.3.2", "tsconfck": "^3.1.6", "typedoc": "^0.28.17", "typedoc-plugin-coverage": "^4.0.2", "typedoc-plugin-markdown": "^4.10.0" }, "peerDependencies": { "prettier": ">=3.0.0" }, "optionalPeers": ["prettier"], "bin": "./dist/bin/orval.mjs" }, "sha512-+8Es2ZR3tPthzAL27X1a9AlboqTQ/w9U/PhMkp4vsLA9OvdkpXr+9f8lCfJUV/wtdX+lXBDQ4imx42Em943JSg=="], @@ -887,12 +919,18 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], @@ -929,8 +967,14 @@ "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], @@ -975,6 +1019,8 @@ "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], + "vitest": ["vitest@4.1.0", "", { "dependencies": { "@vitest/expect": "4.1.0", "@vitest/mocker": "4.1.0", "@vitest/pretty-format": "4.1.0", "@vitest/runner": "4.1.0", "@vitest/snapshot": "4.1.0", "@vitest/spy": "4.1.0", "@vitest/utils": "4.1.0", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.0", "@vitest/browser-preview": "4.1.0", "@vitest/browser-webdriverio": "4.1.0", "@vitest/ui": "4.1.0", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], @@ -985,6 +1031,8 @@ "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], diff --git a/frontend/package.json b/frontend/package.json index f5b1ca4..9f176b5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,13 +5,15 @@ "type": "module", "scripts": { "dev": "vite", + "typecheck": "tsc --noEmit", "build": "tsc && vite build", "preview": "vite preview", "generate:api": "orval", "lint": "eslint src --max-warnings=0", "lint:fix": "eslint src --fix", "format": "prettier --check src", - "format:fix": "prettier --write src" + "format:fix": "prettier --write src", + "test": "vitest run" }, "dependencies": { "@tanstack/react-query": "^5.62.16", @@ -42,6 +44,7 @@ "prettier": "^3.3.3", "tailwindcss": "^3.4.17", "typescript": "~5.6.2", - "vite": "^6.0.5" + "vite": "^6.0.5", + "vitest": "^4.1.0" } } diff --git a/frontend/src/components/ReadingHistoryGraph.test.ts b/frontend/src/components/ReadingHistoryGraph.test.ts index 35fdd65..298958c 100644 --- a/frontend/src/components/ReadingHistoryGraph.test.ts +++ b/frontend/src/components/ReadingHistoryGraph.test.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest'; import { getSVGGraphData } from './ReadingHistoryGraph'; // Test data matching Go test exactly diff --git a/frontend/src/components/ReadingHistoryGraph.tsx b/frontend/src/components/ReadingHistoryGraph.tsx index 8ff3333..9274f21 100644 --- a/frontend/src/components/ReadingHistoryGraph.tsx +++ b/frontend/src/components/ReadingHistoryGraph.tsx @@ -9,9 +9,6 @@ export interface SVGPoint { y: number; } -/** - * Generates bezier control points for smooth curves - */ function getSVGBezierOpposedLine( pointA: SVGPoint, pointB: SVGPoint @@ -19,7 +16,6 @@ function getSVGBezierOpposedLine( const lengthX = pointB.x - pointA.x; const lengthY = pointB.y - pointA.y; - // Go uses int() which truncates toward zero, JavaScript Math.trunc matches this return { Length: Math.floor(Math.sqrt(lengthX * lengthX + lengthY * lengthY)), Angle: Math.trunc(Math.atan2(lengthY, lengthX)), @@ -32,7 +28,6 @@ function getBezierControlPoint( nextPoint: SVGPoint | null, isReverse: boolean ): SVGPoint { - // First / Last Point let pPrev = prevPoint; let pNext = nextPoint; if (!pPrev) { @@ -42,57 +37,49 @@ function getBezierControlPoint( pNext = currentPoint; } - // Modifiers - const smoothingRatio: number = 0.2; - const directionModifier: number = isReverse ? Math.PI : 0; + const smoothingRatio = 0.2; + const directionModifier = isReverse ? Math.PI : 0; const opposingLine = getSVGBezierOpposedLine(pPrev, pNext); - const lineAngle: number = opposingLine.Angle + directionModifier; - const lineLength: number = opposingLine.Length * smoothingRatio; + const lineAngle = opposingLine.Angle + directionModifier; + const lineLength = opposingLine.Length * smoothingRatio; - // Calculate Control Point - Go converts everything to int - // Note: int(math.Cos(...) * lineLength) means truncate product, not truncate then multiply return { x: Math.floor(currentPoint.x + Math.trunc(Math.cos(lineAngle) * lineLength)), y: Math.floor(currentPoint.y + Math.trunc(Math.sin(lineAngle) * lineLength)), }; } -/** - * Generates the bezier path for the graph - */ function getSVGBezierPath(points: SVGPoint[]): string { if (points.length === 0) { return ''; } - let bezierSVGPath: string = ''; + let bezierSVGPath = ''; for (let index = 0; index < points.length; index++) { const point = points[index]; + if (!point) { + continue; + } + if (index === 0) { bezierSVGPath += `M ${point.x},${point.y}`; - } else { - const pointPlusOne = points[index + 1]; - const pointMinusOne = points[index - 1]; - const pointMinusTwo: SVGPoint | null = index - 2 >= 0 ? points[index - 2] : null; - - const startControlPoint: SVGPoint = getBezierControlPoint( - pointMinusOne, - pointMinusTwo, - point, - false - ); - const endControlPoint: SVGPoint = getBezierControlPoint( - point, - pointMinusOne, - pointPlusOne || point, - true - ); - - // Go converts all coordinates to int - bezierSVGPath += ` C${startControlPoint.x},${startControlPoint.y} ${endControlPoint.x},${endControlPoint.y} ${point.x},${point.y}`; + continue; } + + const pointMinusOne = points[index - 1]; + if (!pointMinusOne) { + continue; + } + + const pointPlusOne = points[index + 1] ?? point; + const pointMinusTwo = index - 2 >= 0 ? (points[index - 2] ?? null) : null; + + const startControlPoint = getBezierControlPoint(pointMinusOne, pointMinusTwo, point, false); + const endControlPoint = getBezierControlPoint(point, pointMinusOne, pointPlusOne, true); + + bezierSVGPath += ` C${startControlPoint.x},${startControlPoint.y} ${endControlPoint.x},${endControlPoint.y} ${point.x},${point.y}`; } return bezierSVGPath; @@ -105,42 +92,35 @@ export interface SVGGraphData { Offset: number; } -/** - * Get SVG Graph Data - */ export function getSVGGraphData( inputData: GraphDataPoint[], svgWidth: number, svgHeight: number ): SVGGraphData { - // Derive Height - let maxHeight: number = 0; + let maxHeight = 0; for (const item of inputData) { if (item.minutes_read > maxHeight) { maxHeight = item.minutes_read; } } - // Vertical Graph Real Estate - const sizePercentage: number = 0.5; + const sizePercentage = 0.5; + const sizeRatio = maxHeight > 0 ? (svgHeight * sizePercentage) / maxHeight : 0; + const blockOffset = inputData.length > 0 ? Math.floor(svgWidth / inputData.length) : 0; - // Scale Ratio -> Desired Height - const sizeRatio: number = (svgHeight * sizePercentage) / maxHeight; - - // Point Block Offset - const blockOffset: number = Math.floor(svgWidth / inputData.length); - - // Line & Bar Points const linePoints: SVGPoint[] = []; - // Bezier Fill Coordinates (Max X, Min X, Max Y) - let maxBX: number = 0; - let maxBY: number = 0; - let minBX: number = 0; + let maxBX = 0; + let maxBY = 0; + let minBX = 0; for (let idx = 0; idx < inputData.length; idx++) { - // Go uses int conversion - const itemSize = Math.floor(inputData[idx].minutes_read * sizeRatio); + const item = inputData[idx]; + if (!item) { + continue; + } + + const itemSize = Math.floor(item.minutes_read * sizeRatio); const itemY = svgHeight - itemSize; const lineX = (idx + 1) * blockOffset; @@ -162,7 +142,6 @@ export function getSVGGraphData( } } - // Return Data return { LinePoints: linePoints, BezierPath: getSVGBezierPath(linePoints), @@ -171,27 +150,14 @@ export function getSVGGraphData( }; } -/** - * Formats a date string to YYYY-MM-DD format (ISO-like) - * Note: The date string from the API is already in YYYY-MM-DD format, - * but since JavaScript Date parsing can add timezone offsets, we use UTC - * methods to ensure we get the correct date. - */ function formatDate(dateString: string): string { const date = new Date(dateString); - // Use UTC methods to avoid timezone offset issues const year = date.getUTCFullYear(); const month = String(date.getUTCMonth() + 1).padStart(2, '0'); const day = String(date.getUTCDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } -/** - * ReadingHistoryGraph component - * - * Displays a bezier curve graph of daily reading totals with hover tooltips. - * Exact copy of Go template implementation. - */ export default function ReadingHistoryGraph({ data }: ReadingHistoryGraphProps) { const svgWidth = 800; const svgHeight = 70; @@ -204,11 +170,7 @@ export default function ReadingHistoryGraph({ data }: ReadingHistoryGraphProps) ); } - const { - BezierPath, - BezierFill, - LinePoints: _linePoints, - } = getSVGGraphData(data, svgWidth, svgHeight); + const { BezierPath, BezierFill } = getSVGGraphData(data, svgWidth, svgHeight); return (
@@ -227,7 +189,6 @@ export default function ReadingHistoryGraph({ data }: ReadingHistoryGraphProps) {data.map((point, i) => (
> { +export interface Column { key: keyof T; header: string; render?: (value: T[keyof T], _row: T, _index: number) => React.ReactNode; className?: string; } -export interface TableProps> { +export interface TableProps { columns: Column[]; data: T[]; loading?: boolean; @@ -17,7 +17,6 @@ export interface TableProps> { rowKey?: keyof T | ((row: T) => string); } -// Skeleton table component for loading state function SkeletonTable({ rows = 5, columns = 4, @@ -28,7 +27,7 @@ function SkeletonTable({ className?: string; }) { return ( -
+
@@ -58,19 +57,19 @@ function SkeletonTable({ ); } -export function Table>({ +export function Table({ columns, data, loading = false, emptyMessage = 'No Results', rowKey, }: TableProps) { - const getRowKey = (_row: T, index: number): string => { + const getRowKey = (row: T, index: number): string => { if (typeof rowKey === 'function') { - return rowKey(_row); + return rowKey(row); } if (rowKey) { - return String(_row[rowKey] ?? index); + return String(row[rowKey] ?? index); } return `row-${index}`; }; @@ -113,7 +112,9 @@ export function Table>({ key={`${getRowKey(row, index)}-${String(column.key)}`} className={`p-3 text-gray-700 dark:text-gray-300 ${column.className || ''}`} > - {column.render ? column.render(row[column.key], row, index) : row[column.key]} + {column.render + ? column.render(row[column.key], row, index) + : (row[column.key] as React.ReactNode)} ))} diff --git a/frontend/src/icons/GitIcon.tsx b/frontend/src/icons/GitIcon.tsx index a5f6cbb..2c56c79 100644 --- a/frontend/src/icons/GitIcon.tsx +++ b/frontend/src/icons/GitIcon.tsx @@ -1,9 +1,14 @@ -export function GitIcon() { +interface GitIconProps { + size?: number; + className?: string; +} + +export function GitIcon({ size = 20, className = '' }: GitIconProps) { return ( @@ -22,19 +27,19 @@ export function GitIcon() { style={{ stroke: 'none', fillRule: 'nonzero', fillOpacity: 1 }} d="M130.871 31.836c-4.785 0-8.351 2.352-8.351 8.008 0 4.261 2.347 7.222 8.093 7.222 4.871 0 8.18-2.867 8.18-7.398 0-5.133-2.961-7.832-7.922-7.832Zm-9.57 39.95c-1.133 1.39-2.262 2.87-2.262 4.612 0 3.48 4.434 4.524 10.527 4.524 5.051 0 11.926-.352 11.926-5.043 0-2.793-3.308-2.965-7.488-3.227Zm25.761-39.688c1.563 2.004 3.22 4.789 3.22 8.793 0 9.656-7.571 15.316-18.536 15.316-2.789 0-5.312-.348-6.879-.785l-2.87 4.613 8.526.52c15.059.96 23.934 1.398 23.934 12.968 0 10.008-8.789 15.665-23.934 15.665-15.75 0-21.757-4.004-21.757-10.88 0-3.917 1.742-6 4.789-8.878-2.875-1.211-3.828-3.387-3.828-5.739 0-1.914.953-3.656 2.523-5.312 1.566-1.652 3.305-3.305 5.395-5.219-4.262-2.09-7.485-6.617-7.485-13.058 0-10.008 6.613-16.88 19.93-16.88 3.742 0 6.004.344 8.008.872h16.972v7.394l-8.007.61" /> - + - + - + [] = [ { key: 'document_id' as const, header: 'Document', - render: (_value: Activity['document_id'], row: Activity) => ( + render: (_value, row) => ( value || 'N/A', + render: value => String(value || 'N/A'), }, { key: 'duration' as const, header: 'Duration', - render: (value: Activity['duration']) => { - return formatDuration(value || 0); + render: value => { + return formatDuration(typeof value === 'number' ? value : 0); }, }, { key: 'end_percentage' as const, header: 'Percent', - render: (value: Activity['end_percentage']) => (value != null ? `${value}%` : '0%'), + render: value => (typeof value === 'number' ? `${value}%` : '0%'), }, ]; diff --git a/frontend/src/pages/AdminImportPage.tsx b/frontend/src/pages/AdminImportPage.tsx index 1d4ffca..02abdc0 100644 --- a/frontend/src/pages/AdminImportPage.tsx +++ b/frontend/src/pages/AdminImportPage.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { useGetImportDirectory, usePostImport } from '../generated/anthoLumeAPIV1'; +import type { DirectoryItem, DirectoryListResponse } from '../generated/model'; import { getErrorMessage } from '../utils/errors'; import { Button } from '../components/Button'; import { FolderOpenIcon } from '../icons'; @@ -17,8 +18,10 @@ export default function AdminImportPage() { const postImport = usePostImport(); - const directories = directoryData?.data?.items || []; - const currentPathDisplay = directoryData?.data?.current_path ?? currentPath ?? '/data'; + const directoryResponse = + directoryData?.status === 200 ? (directoryData.data as DirectoryListResponse) : null; + const directories = directoryResponse?.items ?? []; + const currentPathDisplay = directoryResponse?.current_path ?? currentPath ?? '/data'; const handleSelectDirectory = (directory: string) => { setSelectedDirectory(`${currentPath}/${directory}`); @@ -148,7 +151,7 @@ export default function AdminImportPage() { ) : ( - directories.map(item => ( + directories.map((item: DirectoryItem) => (
Loading...
; diff --git a/frontend/src/pages/AdminLogsPage.tsx b/frontend/src/pages/AdminLogsPage.tsx index 376f6e7..91cfe76 100644 --- a/frontend/src/pages/AdminLogsPage.tsx +++ b/frontend/src/pages/AdminLogsPage.tsx @@ -1,5 +1,6 @@ import { useState, FormEvent } from 'react'; import { useGetLogs } from '../generated/anthoLumeAPIV1'; +import type { LogsResponse } from '../generated/model'; import { Button } from '../components/Button'; import { SearchIcon } from '../icons'; @@ -8,7 +9,7 @@ export default function AdminLogsPage() { const { data: logsData, isLoading, refetch } = useGetLogs(filter ? { filter } : {}); - const logs = logsData?.data?.logs || []; + const logs = logsData?.status === 200 ? ((logsData.data as LogsResponse).logs ?? []) : []; const handleFilterSubmit = (e: FormEvent) => { e.preventDefault(); @@ -21,7 +22,6 @@ export default function AdminLogsPage() { return (
- {/* Filter Form */}
@@ -46,14 +46,13 @@ export default function AdminLogsPage() {
- {/* Log Display */}
- {logs.map((log: string, index: number) => ( + {logs.map((log, index) => ( - {log} + {typeof log === 'string' ? log : JSON.stringify(log)} ))}
diff --git a/frontend/src/pages/AdminUsersPage.tsx b/frontend/src/pages/AdminUsersPage.tsx index de02c47..533fb00 100644 --- a/frontend/src/pages/AdminUsersPage.tsx +++ b/frontend/src/pages/AdminUsersPage.tsx @@ -1,5 +1,6 @@ import { useState, FormEvent } from 'react'; import { useGetUsers, useUpdateUser } from '../generated/anthoLumeAPIV1'; +import type { User, UsersResponse } from '../generated/model'; import { AddIcon, DeleteIcon } from '../icons'; import { useToasts } from '../components/ToastContext'; import { getErrorMessage } from '../utils/errors'; @@ -14,7 +15,7 @@ export default function AdminUsersPage() { const [newPassword, setNewPassword] = useState(''); const [newIsAdmin, setNewIsAdmin] = useState(false); - const users = usersData?.data?.users || []; + const users = usersData?.status === 200 ? ((usersData.data as UsersResponse).users ?? []) : []; const handleCreateUser = (e: FormEvent) => { e.preventDefault(); @@ -73,7 +74,7 @@ export default function AdminUsersPage() { data: { operation: 'UPDATE', user: userId, - password: password, + password, }, }, { @@ -116,7 +117,6 @@ export default function AdminUsersPage() { return (
- {/* Add User Form */} {showAddForm && (
)} - {/* Users Table */}
@@ -188,19 +187,16 @@ export default function AdminUsersPage() { ) : ( - users.map(user => ( + users.map((user: User) => ( - {/* Delete Button */} - {/* User ID */} - {/* Password Reset */} - {/* Admin Toggle */} - {/* Created Date */} diff --git a/frontend/src/pages/ProgressPage.tsx b/frontend/src/pages/ProgressPage.tsx index 4406976..e1b7c77 100644 --- a/frontend/src/pages/ProgressPage.tsx +++ b/frontend/src/pages/ProgressPage.tsx @@ -1,17 +1,17 @@ import { Link } from 'react-router-dom'; import { useGetProgressList } from '../generated/anthoLumeAPIV1'; import type { Progress } from '../generated/model'; -import { Table } from '../components/Table'; +import { Table, type Column } from '../components/Table'; export default function ProgressPage() { const { data, isLoading } = useGetProgressList({ page: 1, limit: 15 }); const progress = data?.status === 200 ? (data.data.progress ?? []) : []; - const columns = [ + const columns: Column[] = [ { key: 'document_id' as const, header: 'Document', - render: (_value: Progress['document_id'], row: Progress) => ( + render: (_value, row) => ( value || 'Unknown', + render: value => String(value || 'Unknown'), }, { key: 'percentage' as const, header: 'Percentage', - render: (value: Progress['percentage']) => (value ? `${Math.round(value)}%` : '0%'), + render: value => (typeof value === 'number' ? `${Math.round(value)}%` : '0%'), }, { key: 'created_at' as const, header: 'Created At', - render: (value: Progress['created_at']) => - value ? new Date(value).toLocaleDateString() : 'N/A', + render: value => + typeof value === 'string' && value ? new Date(value).toLocaleDateString() : 'N/A', }, ]; diff --git a/frontend/src/types/window.d.ts b/frontend/src/types/window.d.ts new file mode 100644 index 0000000..ad106bc --- /dev/null +++ b/frontend/src/types/window.d.ts @@ -0,0 +1,22 @@ +interface FilePickerAcceptType { + description?: string; + accept: Record; +} + +interface SaveFilePickerOptions { + suggestedName?: string; + types?: FilePickerAcceptType[]; +} + +interface FileSystemWritableFileStream { + write(data: BufferSource | Blob | string): Promise; + close(): Promise; +} + +interface FileSystemFileHandle { + createWritable(): Promise; +} + +interface Window { + showSaveFilePicker?: (options?: SaveFilePickerOptions) => Promise; +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 270518a..5cbd43c 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -23,4 +23,4 @@ "noUncheckedIndexedAccess": true }, "include": ["src"] -} \ No newline at end of file +}

{user.id}

{user.created_at}