From 891cd19f461974c6f0f760959f475cc8ad530f42 Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:08:15 -0600 Subject: [PATCH] chore: implement local quality foundation --- .husky/.gitignore | 1 + .husky/pre-commit | 5 + .husky/pre-push | 5 + CONTRIBUTING.md | 52 ++++++ README.md | 96 +++++----- client/README.md | 3 +- client/package.json | 49 ++--- client/src/components/ViewHeader.vue | 13 +- .../src/features/auth/composables/useOidc.ts | 1 - .../features/book/components/BookListRow.vue | 14 +- .../__tests__/BookCoverCard.spec.ts | 24 ++- .../components/__tests__/BookListRow.spec.ts | 22 ++- .../components/__tests__/DetailsTab.spec.ts | 5 +- .../detail/tabs/CoverSearchDrawer.vue | 18 +- .../book/composables/useBookNavigation.ts | 2 +- .../reader/components/CbzReaderView.vue | 32 +++- .../features/reader/components/PdfFindBar.vue | 12 +- .../reader/composables/useBookmarks.ts | 2 +- .../reader/composables/usePdfThumbnails.ts | 3 +- .../reader/composables/useReaderSettings.ts | 6 +- .../staging/composables/useStagingDetail.ts | 2 +- .../staging/composables/useStagingFiles.ts | 3 +- client/tsconfig.node.json | 2 +- client/typecheck-baseline.txt | 64 +++++++ client/vitest.config.ts | 11 ++ package.json | 62 ++++++- scripts/client-typecheck-baseline.mjs | 120 ++++++++++++ scripts/db/reset.sh | 28 +++ scripts/db/seed.sh | 7 + scripts/doctor.sh | 26 +++ scripts/help.mjs | 24 +++ scripts/setup.sh | 24 +++ server/README.md | 10 + server/eslint.config.mjs | 8 +- server/package.json | 31 +++- server/src/modules/auth/auth.module.ts | 5 +- server/src/modules/book/book.repository.ts | 16 +- server/src/modules/book/book.service.ts | 15 +- server/src/modules/cover/cover.controller.ts | 16 +- server/src/modules/cover/cover.service.ts | 3 +- server/src/modules/scanner/scanner.module.ts | 5 +- .../src/modules/staging/staging.controller.ts | 10 +- server/src/modules/staging/staging.module.ts | 5 +- server/src/scripts/db-seed.ts | 30 +++ server/test/app.e2e-spec.ts | 173 ++++++++++++++++-- server/test/jest-e2e.json | 15 +- 46 files changed, 891 insertions(+), 189 deletions(-) create mode 100644 .husky/.gitignore create mode 100644 .husky/pre-commit create mode 100644 .husky/pre-push create mode 100644 CONTRIBUTING.md create mode 100644 client/typecheck-baseline.txt create mode 100644 scripts/client-typecheck-baseline.mjs create mode 100644 scripts/db/reset.sh create mode 100644 scripts/db/seed.sh create mode 100644 scripts/doctor.sh create mode 100644 scripts/help.mjs create mode 100644 scripts/setup.sh create mode 100644 server/src/scripts/db-seed.ts diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 000000000..31354ec13 --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 000000000..6a904ee6b --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ +#!/usr/bin/env sh +HUSKY_SH="$(dirname -- "$0")/_/husky.sh" +[ -f "$HUSKY_SH" ] && . "$HUSKY_SH" + +pnpm exec lint-staged diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 000000000..043f7fa24 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,5 @@ +#!/usr/bin/env sh +HUSKY_SH="$(dirname -- "$0")/_/husky.sh" +[ -f "$HUSKY_SH" ] && . "$HUSKY_SH" + +pnpm run quick diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..ecdfc6ac6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,52 @@ +# Contributing + +## Daily Workflow + +Use this command surface: + +1. `pnpm setup` (once per machine) +2. `pnpm dev` (daily coding loop) +3. `pnpm verify` (before push) + +Optional: + +1. `pnpm quick` (faster checks while coding) +2. `pnpm verify:strict` (aspirational strict gate while we burn down baseline debt) +3. `pnpm guide` (show command reference) + +## Quality Gates + +Before opening or updating a PR: + +1. `pnpm verify` must pass +2. `pnpm verify:strict` is currently optional until formatting and full client typecheck baseline debt is reduced. +3. If tests touched critical flows, run `pnpm test:e2e:smoke` +4. If coverage-sensitive changes are made, run `pnpm coverage` + +## Check vs Fix Commands + +Non-mutating: + +1. `pnpm format:check` +2. `pnpm lint:check` +3. `pnpm typecheck` +4. `pnpm typecheck:full` +5. `pnpm typecheck:client:baseline:update` (update accepted client TS baseline after intentional changes) + +Mutating: + +1. `pnpm format` +2. `pnpm lint:fix` + +## Database Commands + +1. `pnpm db:up` +2. `pnpm db:migrate` +3. `pnpm db:seed` +4. `pnpm db:reset` + +## Troubleshooting + +1. Run `pnpm doctor` for environment checks. +2. If dependencies changed, run `pnpm install`. +3. If local DB state is inconsistent, run `pnpm db:reset`. diff --git a/README.md b/README.md index 333072c5c..7fb229dd5 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ A book and library management app with Kobo device support. Multi-user, self-hos ### Prerequisites | Tool | Version | Install | -|---------|---------|--------------------------------------------------------------| +| ------- | ------- | ------------------------------------------------------------ | | Node.js | >= 24 | [nodejs.org](https://nodejs.org) | | pnpm | >= 9 | `corepack enable && corepack prepare pnpm@latest --activate` | | Docker | latest | [docker.com](https://www.docker.com/products/docker-desktop) | @@ -21,10 +21,7 @@ A book and library management app with Kobo device support. Multi-user, self-hos ```bash git clone projectx cd projectx -docker compose up -d --wait # start Postgres -cp server/.env.example server/.env # create env config (edit if needed) -pnpm install # install all dependencies -cd server && pnpm db:migrate && cd .. # apply database migrations +pnpm setup ``` Docker is only used for PostgreSQL. The server and client run directly on your machine. @@ -55,8 +52,8 @@ Both must be running. The client proxies all `/api` and `/socket.io` requests to ### First steps after setup 1. **Log in** - a default admin account is created automatically: - - Username: `admin` - - Password: `admin` + - Username: `admin` + - Password: `admin` 2. **Change password** - you'll be prompted to set a new password on first login. Use `Admin123!` for local dev. 3. **Create a library** - open Settings (gear icon) > Libraries > Create Library. Add a folder path pointing to some books on your machine, e.g. any directory containing `.epub`, `.pdf`, or `.cbz` files. 4. **Scan** - the library scans automatically after creation. You should see books appear on the home page once the scan completes. @@ -95,40 +92,58 @@ This is a **pnpm workspace**. The three packages are: All commands run from the **repo root** unless noted otherwise. +### Everyday workflow + +| Command | Description | +| -------------------- | -------------------------------------------------- | +| `pnpm setup` | One-time bootstrap | +| `pnpm dev` | Daily development | +| `pnpm verify` | Default local checks before push | +| `pnpm quick` | Faster local checks while coding | +| `pnpm verify:strict` | Aspirational strict gate (format + full typecheck) | +| `pnpm guide` | Print command reference | + ### Development | Command | Description | -|-------------------------------|--------------------------------------| +| ----------------------------- | ------------------------------------ | | `pnpm dev` | Start server + client (concurrently) | | `cd server && pnpm start:dev` | Server only (watch mode) | | `cd client && pnpm dev` | Client only (Vite dev server) | ### Testing -| Command | Description | -|----------------------------------------------------|---------------------------| -| `cd server && pnpm test` | Server unit tests | -| `cd server && pnpm test -- --testPathPattern=scan` | Run matching server tests | -| `cd client && pnpm test:unit` | Client unit tests | -| `cd client && pnpm test:unit -- BookCover` | Run matching client tests | -| `cd server && pnpm test:e2e` | Server E2E tests | +| Command | Description | +| ------------------------------- | ---------------------------- | +| `pnpm test` | Server + client unit tests | +| `pnpm test:server` | Server unit tests | +| `pnpm test:client` | Client unit tests | +| `pnpm test:e2e:smoke` | Server smoke e2e | +| `pnpm coverage` | Coverage for server + client | +| `pnpm --filter server test:e2e` | Server e2e only | ### Database -| Command | Description | -|----------------------------------------|------------------------------------------| -| `cd server && pnpm db:migrate` | Apply pending migrations | -| `cd server && pnpm db:generate ` | Generate a migration from schema changes | -| `cd server && pnpm db:studio` | Open Drizzle Studio (DB browser) | +| Command | Description | +| ----------------------------------------- | ---------------------------------------- | +| `pnpm db:up` | Start local Postgres | +| `pnpm db:migrate` | Apply pending migrations | +| `pnpm db:seed` | Seed baseline app data | +| `pnpm db:reset` | Reset schema, migrate, and seed | +| `pnpm --filter server db:generate ` | Generate a migration from schema changes | +| `pnpm --filter server db:studio` | Open Drizzle Studio (DB browser) | ### Linting & formatting -| Command | Description | -|---------------------------------------|--------------------| -| `cd server && npx prettier --write .` | Format server code | -| `cd client && npx prettier --write .` | Format client code | -| `cd server && pnpm lint` | Lint server | -| `cd client && pnpm lint` | Lint client | +| Command | Description | +| --------------------------------------- | ------------------------------------------ | +| `pnpm format:check` | Check formatting | +| `pnpm format` | Auto-format source files | +| `pnpm lint:check` | Non-mutating lint checks | +| `pnpm lint:fix` | Auto-fix lint issues | +| `pnpm typecheck` | Server typecheck + no-new client TS errors | +| `pnpm typecheck:full` | Typecheck server + client (strict) | +| `pnpm typecheck:client:baseline:update` | Refresh accepted client TS baseline | --- @@ -162,7 +177,7 @@ If other people have pushed changes, you may need to sync dependencies and migra ```bash pnpm install # pick up lockfile changes -cd server && pnpm db:migrate # apply any new migrations +pnpm db:migrate # apply any new migrations ``` ### Adding a dependency @@ -189,7 +204,7 @@ The fastest way to learn the patterns is to follow an existing module: Server environment is configured in `server/.env` (created from `.env.example` during setup). | Variable | Default | Description | -|--------------------------|--------------------------------------------------------|------------------------------------------------------------| +| ------------------------ | ------------------------------------------------------ | ---------------------------------------------------------- | | `DATABASE_URL` | `postgres://projectx:projectx@localhost:5432/projectx` | PostgreSQL connection string | | `PORT` | `3000` | Server port | | `NODE_ENV` | `development` | Environment mode | @@ -209,31 +224,17 @@ The database schema is defined in `server/src/db/schema/` using Drizzle ORM. Eac **Making schema changes:** 1. Edit the relevant file in `server/src/db/schema/` -2. Generate a migration: `cd server && pnpm db:generate describe_your_change` -3. Apply it: `cd server && pnpm db:migrate` +2. Generate a migration: `pnpm --filter server db:generate describe_your_change` +3. Apply it: `pnpm db:migrate` Never hand-write migration SQL. Always generate from schema diffs. ### Resetting the database from scratch ```bash -# 1. Drop and recreate the public schema -docker exec $(docker ps --filter "name=postgres" --format "{{.Names}}" | head -1) \ - psql -U projectx -d projectx -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" - -# 2. Clear Drizzle's migration tracking (it lives in a separate schema) -docker exec $(docker ps --filter "name=postgres" --format "{{.Names}}" | head -1) \ - psql -U projectx -d projectx -c "DELETE FROM drizzle.__drizzle_migrations;" - -# 3. Re-apply all migrations -cd server && pnpm db:migrate - -# 4. (Optional) Wipe cover images -rm -rf local/data/covers +pnpm db:reset ``` -Step 2 is necessary because Drizzle tracks applied migrations in `drizzle.__drizzle_migrations`, which lives outside the `public` schema and survives the drop. - --- ## Architecture Overview @@ -264,7 +265,7 @@ Another PostgreSQL instance is running. Either stop it or change the port mappin ```yaml ports: - - "5433:5432" # use 5433 on host + - "5433:5432" # use 5433 on host ``` Then update `DATABASE_URL` in `server/.env` to use port `5433`. @@ -325,8 +326,7 @@ Make sure you're accessing the client at `http://localhost:5173` (not port 3000) ## Further Reading | Doc | What it covers | -|----------------------------------------|--------------------------------------------------------------| +| -------------------------------------- | ------------------------------------------------------------ | | [`server/README.md`](server/README.md) | Backend module map, DB commands, NestJS conventions | | [`client/README.md`](client/README.md) | Frontend project layout, IDE setup, Vue/Tailwind conventions | | [`packages/types/`](packages/types/) | Shared type definitions between server and client | - diff --git a/client/README.md b/client/README.md index f3e76349e..7fed33758 100644 --- a/client/README.md +++ b/client/README.md @@ -43,7 +43,8 @@ src/ ## Testing ```bash -pnpm test:unit # run Vitest +pnpm test:unit # run unit tests +pnpm test:cov # run unit tests with coverage ``` ## Key conventions diff --git a/client/package.json b/client/package.json index 0103f9521..8e175954e 100644 --- a/client/package.json +++ b/client/package.json @@ -8,57 +8,64 @@ "build": "run-p type-check \"build-only {@}\" --", "preview": "vite preview", "test:unit": "vitest", + "test:cov": "vitest --coverage", "build-only": "vite build", "type-check": "vue-tsc --build", - "lint": "run-s lint:*", - "lint:oxlint": "oxlint . --fix", - "lint:eslint": "eslint . --fix --cache", - "format": "prettier --write --experimental-cli src/" + "lint": "pnpm run lint:check", + "lint:check": "run-s lint:oxlint:check lint:eslint:check", + "lint:fix": "run-s lint:oxlint:fix lint:eslint:fix", + "lint:oxlint:check": "oxlint src", + "lint:oxlint:fix": "oxlint src --fix", + "lint:eslint:check": "eslint src --cache", + "lint:eslint:fix": "eslint src --fix --cache", + "format": "prettier --write --experimental-cli src/", + "format:check": "prettier --check src/" }, "dependencies": { "@fontsource-variable/fraunces": "^5.2.9", "@fontsource-variable/inter": "^5.2.8", "@projectx/types": "workspace:*", - "@tailwindcss/vite": "^4.1.18", + "@tailwindcss/vite": "^4.2.1", "@vueuse/core": "^14.2.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "lucide-vue-next": "^0.564.0", - "pdfjs-dist": "^5.4.624", + "lucide-vue-next": "^0.577.0", + "pdfjs-dist": "^5.5.207", "pinia": "^3.0.4", - "reka-ui": "^2.8.0", + "reka-ui": "^2.9.0", "socket.io-client": "^4.8.3", - "tailwind-merge": "^3.4.0", - "tailwindcss": "^4.1.18", - "vue": "^3.5.28", - "vue-router": "^5.0.2", + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.2.1", + "vue": "^3.5.29", + "vue-router": "^5.0.3", "vue-sonner": "^2.0.9", "vue-virtual-scroller": "^2.0.0-beta.8" }, "devDependencies": { "@tsconfig/node24": "^24.0.4", - "@types/jsdom": "^27.0.0", - "@types/node": "^24.10.13", + "@types/jsdom": "^28.0.0", + "@types/node": "^24.11.0", "@vitejs/plugin-vue": "^6.0.4", - "@vitest/eslint-plugin": "^1.6.7", - "@vue/eslint-config-typescript": "^14.6.0", + "@vitest/coverage-v8": "^4.0.18", + "@vitest/eslint-plugin": "^1.6.9", + "@vue/eslint-config-typescript": "^14.7.0", "@vue/test-utils": "^2.4.6", "@vue/tsconfig": "^0.8.1", - "eslint": "^10.0.0", + "eslint": "^10.0.2", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-oxlint": "~1.46.0", + "eslint-plugin-oxlint": "~1.50.0", "eslint-plugin-vue": "~10.8.0", "jiti": "^2.6.1", - "jsdom": "^28.0.0", + "jsdom": "^28.1.0", "npm-run-all2": "^8.0.4", - "oxlint": "~1.47.0", + "oxlint": "~1.50.0", "prettier": "3.8.1", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", "vite": "^7.3.1", "vite-plugin-vue-devtools": "^8.0.6", "vitest": "^4.0.18", - "vue-tsc": "^3.2.4" + "vue-tsc": "^3.2.5" }, "engines": { "node": "^20.19.0 || >=22.12.0" diff --git a/client/src/components/ViewHeader.vue b/client/src/components/ViewHeader.vue index bf65f932c..7bef14717 100644 --- a/client/src/components/ViewHeader.vue +++ b/client/src/components/ViewHeader.vue @@ -4,7 +4,6 @@ import { CheckSquare, LayoutGrid, List, MoreHorizontal, SlidersHorizontal, Squar import * as LucideIcons from 'lucide-vue-next' import { Button } from '@/components/ui/button' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' -import { Separator } from '@/components/ui/separator' import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet' import { DropdownMenu, @@ -64,7 +63,11 @@ function getIconComponent(name: string) { variant="ghost" size="sm" class="hidden md:flex h-8 gap-1.5 text-[11px] font-bold uppercase tracking-tight px-2.5 rounded-lg transition-all" - :class="selectionMode ? 'text-primary bg-primary/10 hover:bg-primary/20 ring-1 ring-primary/20' : 'text-muted-foreground/60 hover:text-foreground hover:bg-primary/5'" + :class=" + selectionMode + ? 'text-primary bg-primary/10 hover:bg-primary/20 ring-1 ring-primary/20' + : 'text-muted-foreground/60 hover:text-foreground hover:bg-primary/5' + " @click="emit('toggle-selection')" > @@ -101,7 +104,11 @@ function getIconComponent(name: string) { - diff --git a/client/src/features/auth/composables/useOidc.ts b/client/src/features/auth/composables/useOidc.ts index db71c3238..e09a80040 100644 --- a/client/src/features/auth/composables/useOidc.ts +++ b/client/src/features/auth/composables/useOidc.ts @@ -1,5 +1,4 @@ import type { OidcPublicConfig } from '@projectx/types' -import { api } from '@/lib/api' async function generatePkce(): Promise<{ codeVerifier: string; codeChallenge: string }> { const array = new Uint8Array(32) diff --git a/client/src/features/book/components/BookListRow.vue b/client/src/features/book/components/BookListRow.vue index 29d551b4b..2902ed51a 100644 --- a/client/src/features/book/components/BookListRow.vue +++ b/client/src/features/book/components/BookListRow.vue @@ -104,8 +104,12 @@ function openFile(file: BookFileRef) {