From 1418957b6fa8cc258a0b0ddad5b9e0cd801d0bfc Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Wed, 25 Mar 2026 07:14:51 -0600 Subject: [PATCH] chore: cleanup project structure and resolve client type errors - resolve all 69 client type-checking errors for stricter TS compliance - remove obsolete type-check baseline scripts and files - overhaul project root with updated README.md and new DEVELOPMENT.md - migrate to unified baseline migration strategy and cleanup docker configs - remove redundant production and dev-specific shell scripts --- .dockerignore | 1 - .env.example | 42 ++++ .env.prod.example | 31 --- .github/workflows/ci.yml | 4 +- .gitignore | 2 +- .husky/pre-push | 2 +- DEVELOPMENT.md | 182 ++++++++++++++++++ Dockerfile | 2 +- Dockerfile.dev | 16 -- client/Dockerfile.dev | 8 - client/src/features/audit/AuditLogPage.vue | 6 +- .../detail/tabs/MetadataDiffPanel.vue | 3 +- .../__tests__/useMetadataDiff.spec.ts | 8 +- .../components/DashboardSettingsSheet.vue | 6 +- .../epub/composables/useFoliateInput.ts | 6 +- .../reader/epub/composables/useReaderState.ts | 6 +- client/src/features/reader/epub/utils.ts | 2 +- .../reader/pdf/composables/usePdfFind.ts | 4 +- .../reader/pdf/composables/usePdfRenderer.ts | 2 +- .../staging/components/StagingFileSheet.vue | 6 +- client/src/lib/api.ts | 8 +- client/typecheck-baseline.txt | 53 ----- client/typecheck_output.txt | 161 ---------------- docker-compose.dev.yml | 24 +++ docker-compose.prod.yml | 106 ---------- docker-compose.yml | 68 +++++-- docs/production.md | 84 -------- package.json | 18 +- packages/types/src/audit.ts | 1 - scripts/client-typecheck-baseline.mjs | 120 ------------ scripts/db/backup-prod.sh | 31 --- scripts/db/reset.sh | 16 +- scripts/db/restore-prod.sh | 35 ---- scripts/db/restore-test-prod.sh | 48 ----- scripts/doctor.sh | 26 --- scripts/help.mjs | 24 --- scripts/seed/reading-sessions.sh | 140 -------------- scripts/setup.sh | 4 +- server/.env.example | 9 +- server/Dockerfile.dev | 6 - server/entrypoint.sh | 9 + server/package.json | 4 +- server/src/app.module.ts | 4 +- server/src/config/config.ts | 8 +- server/src/config/env.validation.ts | 6 +- server/src/db/migrations/0000_baseline.sql | 1 - .../src/db/migrations/meta/0000_snapshot.json | 8 +- server/src/db/migrations/meta/_journal.json | 2 +- server/src/db/schema/audit.ts | 3 +- server/src/modules/audit/audit.service.ts | 4 - server/src/scripts/migrate.ts | 6 + 51 files changed, 379 insertions(+), 997 deletions(-) create mode 100644 .env.example delete mode 100644 .env.prod.example mode change 100644 => 100755 .husky/pre-push create mode 100644 DEVELOPMENT.md delete mode 100644 Dockerfile.dev delete mode 100644 client/Dockerfile.dev delete mode 100644 client/typecheck-baseline.txt delete mode 100644 client/typecheck_output.txt create mode 100644 docker-compose.dev.yml delete mode 100644 docker-compose.prod.yml delete mode 100644 docs/production.md delete mode 100644 scripts/client-typecheck-baseline.mjs delete mode 100755 scripts/db/backup-prod.sh delete mode 100755 scripts/db/restore-prod.sh delete mode 100755 scripts/db/restore-test-prod.sh delete mode 100644 scripts/doctor.sh delete mode 100644 scripts/help.mjs delete mode 100755 scripts/seed/reading-sessions.sh delete mode 100644 server/Dockerfile.dev diff --git a/.dockerignore b/.dockerignore index 1e69499f3..fcb52cf3e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,7 +3,6 @@ **/dist **/*.log .env -.env.prod client/.env server/.env docker/postgres/data diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..60f8dda03 --- /dev/null +++ b/.env.example @@ -0,0 +1,42 @@ +# Immutable image reference built by CI (use a sha-* tag or @sha256 digest) +APP_IMAGE=ghcr.io/your-org/projectx:sha- + +# App publish port on host +APP_PORT=3000 + +# Database credentials +POSTGRES_USER=projectx +POSTGRES_PASSWORD=change-this-password +POSTGRES_DB=projectx + +# App secrets +JWT_SECRET=change-this-to-a-long-random-secret +# Required for the initial /auth/setup endpoint (x-setup-token header) +SETUP_BOOTSTRAP_TOKEN=change-this-to-a-long-random-secret + +# External/public URL used by emails and Kobo endpoints +APP_URL=https://projectx.example.com + +# --- Optional overrides (uncomment to set) --- + +# If you already run your own PostgreSQL, set DATABASE_URL and remove the +# postgres service from docker-compose.yml. Your instance must have the +# uuid-ossp, pg_trgm, and vector (pgvector) extensions available. +# DATABASE_URL=postgres://user:password@yourhost:5432/projectx +# Or set individual parts (POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB) + +# JWT token lifetimes +# JWT_EXPIRES_IN=15m +# JWT_REFRESH_EXPIRES_IN=7d + +# CORS origin for the frontend when served from a different domain than APP_URL +# CLIENT_URL=https://app.example.com + +# Staging directory (defaults to {BOOKS_PATH}/staging inside the container) +# STAGING_PATH=/staging + +# AES-256 key for encrypting stored email credentials (generate: openssl rand -hex 32) +# EMAIL_ENCRYPTION_KEY= + +# Log level (default: info). Set to debug for verbose output. +# LOG_LEVEL=info diff --git a/.env.prod.example b/.env.prod.example deleted file mode 100644 index 2ffa60bc9..000000000 --- a/.env.prod.example +++ /dev/null @@ -1,31 +0,0 @@ -# Immutable image reference built by CI (use a sha-* tag or @sha256 digest) -APP_IMAGE=ghcr.io/your-org/projectx:sha- - -# App publish port on host -APP_PORT=3000 - -# Database credentials -POSTGRES_USER=projectx -POSTGRES_PASSWORD=change-this-password -POSTGRES_DB=projectx - -# App secrets -JWT_SECRET=change-this-to-a-long-random-secret -EMAIL_ENCRYPTION_KEY= - -# External/public URL used by emails and Kobo endpoints -APP_URL=https://projectx.example.com - -# Optional client URL override -CLIENT_URL=https://projectx.example.com - -# Optional SMTP config -SMTP_HOST= -SMTP_PORT=587 -SMTP_SECURE=false -SMTP_USER= -SMTP_PASS= -SMTP_FROM=noreply@projectx.local - -# Optional API keys -GOOGLE_BOOKS_API_KEY= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 468381f58..1e87602e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,7 +62,7 @@ jobs: - name: Compute changed scopes id: changes if: github.event_name != 'workflow_dispatch' - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 + uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4 with: filters: | server: @@ -152,7 +152,7 @@ jobs: pnpm --filter server type-check else pnpm --filter @projectx/types build - pnpm run typecheck:client:baseline + pnpm --filter client type-check fi - name: Prepare test report directory diff --git a/.gitignore b/.gitignore index 541cd6673..b7f0a1b93 100644 --- a/.gitignore +++ b/.gitignore @@ -2,10 +2,10 @@ node_modules/ dist/ .env .env.local -.env.prod client/.env server/.env *.local +data # OS .DS_Store diff --git a/.husky/pre-push b/.husky/pre-push old mode 100644 new mode 100755 index 917fc1a4d..e01991635 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1 +1 @@ -pnpm run quick +pnpm run verify:fast diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 000000000..21211005f --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,182 @@ +# Development Guide + +## Prerequisites + +- Node.js >= 24 +- pnpm >= 9 +- Docker + +--- + +## First-time Setup + +```bash +pnpm setup +``` + +This creates `server/.env` from the example, installs all dependencies, starts PostgreSQL, runs migrations, and seeds baseline data. + +If you prefer to do it manually: + +```bash +pnpm run db:up # start PostgreSQL +cp server/.env.example server/.env # first time only +pnpm install +pnpm run db:migrate +pnpm run db:seed +``` + +--- + +## Running the App + +```bash +pnpm dev +``` + +Starts the server and client concurrently with hot reload. + +| Service | URL | +| ------- | ------------------------- | +| API | http://localhost:3000/api | +| Client | http://localhost:5173 | + +If PostgreSQL stopped (e.g. after a machine restart): + +```bash +pnpm run db:up # then pnpm dev +``` + +--- + +## Database Workflows + +### Apply pending migrations + +After pulling changes that include new migrations: + +```bash +pnpm run db:migrate +``` + +### Add a schema change + +1. Edit the relevant file in `server/src/db/schema/` +2. Generate a migration: + ```bash + cd server && pnpm db:generate add-book-tags + ``` + This creates a new SQL file in `server/src/db/migrations/`. Review it before applying. +3. Apply it: + ```bash + pnpm run db:migrate + ``` + +> **Pre-launch rule:** Before the app ships, maintain a single baseline migration (`0000_*.sql`) rather than stacking incremental ones. When adding a schema change pre-launch: +> +> ```bash +> rm -rf server/src/db/migrations +> docker compose -f docker-compose.dev.yml down -v +> pnpm run db:up +> cd server && pnpm db:generate baseline +> pnpm run db:migrate +> ``` +> +> After go-live, never wipe migrations. Every change gets its own incremental migration. + +### Full database reset + +Wipes all data, re-runs migrations, re-seeds, and clears generated files (covers, author images): + +```bash +pnpm run db:reset +``` + +Use this when: + +- Migrations are in a broken or inconsistent state +- A migration requires starting from scratch (e.g. column type change with no clean upgrade path) +- You want a known-clean local state + +### Inspect the database + +```bash +cd server && pnpm db:studio +``` + +Opens Drizzle Studio in your browser for browsing and editing tables directly. + +--- + +## Testing + +```bash +pnpm run test # all tests (server + client) +pnpm run test:server # server unit tests only +pnpm run test:client # client unit tests only +pnpm run test:e2e:smoke # e2e smoke suite (requires running DB) +``` + +Watch mode while working on a specific area: + +```bash +cd server && pnpm test:watch +``` + +--- + +## Code Quality + +Run before pushing: + +```bash +pnpm run verify:fast # lint + typecheck + tests (same as pre-push hook) +pnpm run verify # above + e2e smoke (run before opening a PR) +``` + +Individual checks: + +```bash +pnpm run lint:check # check for lint errors +pnpm run lint:fix # auto-fix lint errors +pnpm run typecheck # server + client baseline typecheck +pnpm run typecheck:full # server + full client typecheck (slower) +``` + +Format: + +```bash +cd server && npx prettier --write . +cd client && npx prettier --write . +``` + +--- + +## Common Scenarios + +### Pulled changes that include a new migration + +```bash +pnpm run db:migrate +``` + +### Pulled changes that changed the DB schema significantly and your local state is stale + +```bash +pnpm run db:reset +``` + +### Something is wrong and you want a completely clean slate + +```bash +pnpm run db:reset +``` + +If that is not enough (e.g. Docker volume is corrupted): + +```bash +docker compose -f docker-compose.dev.yml down -v +pnpm run db:up +pnpm run db:migrate +pnpm run db:seed +``` diff --git a/Dockerfile b/Dockerfile index 2045ce45f..159a6c910 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,7 +44,7 @@ COPY --from=server-builder --chown=node:node /deploy ./ COPY --from=client-builder --chown=node:node /app/client/dist ./public COPY --from=server-builder --chown=node:node /app/server/entrypoint.sh ./entrypoint.sh -RUN chmod +x /app/entrypoint.sh && mkdir -p /books /tmp && chown -R node:node /books /tmp +RUN chmod +x /app/entrypoint.sh && mkdir -p /books /staging /data /tmp && chown -R node:node /books /staging /data /tmp USER node diff --git a/Dockerfile.dev b/Dockerfile.dev deleted file mode 100644 index 2af593d53..000000000 --- a/Dockerfile.dev +++ /dev/null @@ -1,16 +0,0 @@ -FROM node:24-alpine -RUN corepack enable && corepack prepare pnpm@latest --activate -WORKDIR /app - -COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ -COPY client/package.json ./client/ -COPY server/package.json ./server/ -COPY packages/types/package.json ./packages/types/ - -RUN pnpm install --frozen-lockfile - -COPY . . - -EXPOSE 3000 5173 - -CMD ["pnpm", "dev"] diff --git a/client/Dockerfile.dev b/client/Dockerfile.dev deleted file mode 100644 index 45efc36a7..000000000 --- a/client/Dockerfile.dev +++ /dev/null @@ -1,8 +0,0 @@ -FROM node:24-alpine -WORKDIR /app -RUN npm install -g pnpm -COPY package.json package-lock.json ./ -RUN npm install -COPY . . -EXPOSE 5173 -CMD ["npm", "run", "dev", "--", "--host"] diff --git a/client/src/features/audit/AuditLogPage.vue b/client/src/features/audit/AuditLogPage.vue index cfe647c00..37d996250 100644 --- a/client/src/features/audit/AuditLogPage.vue +++ b/client/src/features/audit/AuditLogPage.vue @@ -101,15 +101,14 @@ onMounted(fetchPage) Action Description IP - Country - Loading... + Loading... - No audit logs found + No audit logs found {{ entry.description }} {{ entry.ip ?? '-' }} - {{ entry.countryCode ?? '-' }} diff --git a/client/src/features/book/components/detail/tabs/MetadataDiffPanel.vue b/client/src/features/book/components/detail/tabs/MetadataDiffPanel.vue index 34281927a..1000d1b42 100644 --- a/client/src/features/book/components/detail/tabs/MetadataDiffPanel.vue +++ b/client/src/features/book/components/detail/tabs/MetadataDiffPanel.vue @@ -59,7 +59,8 @@ watch(representativeCandidates, (list) => { }) const activeCandidate = computed( - () => representativeCandidates.value.find((c) => c.provider === activeProvider.value) ?? representativeCandidates.value[0], + () => + representativeCandidates.value.find((c) => c.provider === activeProvider.value) ?? representativeCandidates.value[0] ?? props.initialCandidate, ) const { fields, picksPerProvider, toggleField, pickFieldFromProvider, clearPicksForProvider, copyAll, copyMissing, buildPatch, hasCopied } = diff --git a/client/src/features/book/composables/__tests__/useMetadataDiff.spec.ts b/client/src/features/book/composables/__tests__/useMetadataDiff.spec.ts index d8e0ae308..00e0ddffc 100644 --- a/client/src/features/book/composables/__tests__/useMetadataDiff.spec.ts +++ b/client/src/features/book/composables/__tests__/useMetadataDiff.spec.ts @@ -6,6 +6,7 @@ import { useMetadataDiff } from '../useMetadataDiff' describe('useMetadataDiff', () => { const mockCurrent: MetadataSource = { title: 'Original Title', + subtitle: 'Original Subtitle', authors: ['Original Author'], genres: ['Genre 1'], description: 'Original Description', @@ -17,6 +18,9 @@ describe('useMetadataDiff', () => { seriesIndex: 1, isbn10: '1234567890', isbn13: '1234567890123', + narrators: [], + durationSeconds: null, + abridged: null, } const mockCandidate1: MetadataCandidate = { @@ -37,8 +41,8 @@ describe('useMetadataDiff', () => { } const providers = [ - { key: 'google' as MetadataProviderKey, label: 'Google Books' }, - { key: 'goodreads' as MetadataProviderKey, label: 'Goodreads' }, + { key: 'google' as MetadataProviderKey, label: 'Google Books', identifiable: true }, + { key: 'goodreads' as MetadataProviderKey, label: 'Goodreads', identifiable: true }, ] it('initializes with fields from active provider', () => { diff --git a/client/src/features/dashboard/components/DashboardSettingsSheet.vue b/client/src/features/dashboard/components/DashboardSettingsSheet.vue index d52ae99b6..66559e4f1 100644 --- a/client/src/features/dashboard/components/DashboardSettingsSheet.vue +++ b/client/src/features/dashboard/components/DashboardSettingsSheet.vue @@ -42,8 +42,10 @@ function onDrop(index: number) { if (draggedIndex.value === null || draggedIndex.value === index) return const items = [...draft.value] const [moved] = items.splice(draggedIndex.value, 1) - items.splice(index, 0, moved) - draft.value = items + if (moved) { + items.splice(index, 0, moved) + draft.value = items + } draggedIndex.value = null dragOverIndex.value = null } diff --git a/client/src/features/reader/epub/composables/useFoliateInput.ts b/client/src/features/reader/epub/composables/useFoliateInput.ts index fdba22ffd..fb125c51f 100644 --- a/client/src/features/reader/epub/composables/useFoliateInput.ts +++ b/client/src/features/reader/epub/composables/useFoliateInput.ts @@ -27,7 +27,7 @@ export function useFoliateInput( function handleTouchStart(e: TouchEvent) { if (e.touches.length !== 1) return - const touch = e.touches[0] + const touch = e.touches[0]! touchStartX = touch.clientX touchStartY = touch.clientY touchStartTime = Date.now() @@ -39,7 +39,7 @@ export function useFoliateInput( function handleTouchMove(e: TouchEvent, doc: Document) { if (e.touches.length !== 1) return - const touch = e.touches[0] + const touch = e.touches[0]! const deltaX = Math.abs(touch.clientX - touchStartX) const deltaY = Math.abs(touch.clientY - touchStartY) const selection = doc.defaultView?.getSelection() @@ -67,7 +67,7 @@ export function useFoliateInput( } if (!isTextSelectionInProgress && e.changedTouches.length === 1) { - const touch = e.changedTouches[0] + const touch = e.changedTouches[0]! const deltaX = touch.clientX - touchStartX const deltaY = Math.abs(touch.clientY - touchStartY) diff --git a/client/src/features/reader/epub/composables/useReaderState.ts b/client/src/features/reader/epub/composables/useReaderState.ts index dfcacf6e0..adc252cb2 100644 --- a/client/src/features/reader/epub/composables/useReaderState.ts +++ b/client/src/features/reader/epub/composables/useReaderState.ts @@ -1,6 +1,6 @@ import { computed, ref } from 'vue' import { themes } from '../constants/themes' -import type { ThemeMode } from '../constants/themes' +import type { Theme, ThemeMode } from '../constants/themes' import type { FoliateRenderer } from './useFoliate' export interface ReaderState { @@ -62,7 +62,7 @@ export function useReaderState() { flow: flow.value, })) - const currentTheme = computed(() => themes.find((t) => t.name === themeName.value) ?? themes[0]) + const currentTheme = computed(() => themes.find((t) => t.name === themeName.value) ?? themes[0]!) const activeMode = computed(() => { const theme = currentTheme.value @@ -209,7 +209,7 @@ export function useReaderState() { function applyToRenderer(renderer: FoliateRenderer): void { if (!renderer) return const s = state.value - renderer.setAttribute('max-column-count', s.maxColumnCount) + renderer.setAttribute('max-column-count', String(s.maxColumnCount)) renderer.setAttribute('gap', `${s.gap * 100}%`) renderer.setAttribute('max-inline-size', `${s.maxInlineSize}px`) renderer.setAttribute('max-block-size', `${s.maxBlockSize}px`) diff --git a/client/src/features/reader/epub/utils.ts b/client/src/features/reader/epub/utils.ts index d044811d6..79599c803 100644 --- a/client/src/features/reader/epub/utils.ts +++ b/client/src/features/reader/epub/utils.ts @@ -1,5 +1,5 @@ export function stripFragment(href: string): string { - return href.split('#')[0] + return href.split('#')[0]! } export function formatDate(dateStr: string): string { diff --git a/client/src/features/reader/pdf/composables/usePdfFind.ts b/client/src/features/reader/pdf/composables/usePdfFind.ts index 1b7872eaa..0de7ce31f 100644 --- a/client/src/features/reader/pdf/composables/usePdfFind.ts +++ b/client/src/features/reader/pdf/composables/usePdfFind.ts @@ -28,8 +28,8 @@ export function usePdfFind(textLayerMap: Ref>) { } function isWordBoundary(text: string, start: number, len: number): boolean { - const before = start === 0 ? ' ' : text[start - 1] - const after = start + len >= text.length ? ' ' : text[start + len] + const before = start === 0 ? ' ' : (text[start - 1] ?? ' ') + const after = start + len >= text.length ? ' ' : (text[start + len] ?? ' ') return /\W/.test(before) && /\W/.test(after) } diff --git a/client/src/features/reader/pdf/composables/usePdfRenderer.ts b/client/src/features/reader/pdf/composables/usePdfRenderer.ts index c008e3b33..04807e8d8 100644 --- a/client/src/features/reader/pdf/composables/usePdfRenderer.ts +++ b/client/src/features/reader/pdf/composables/usePdfRenderer.ts @@ -89,7 +89,7 @@ export function usePdfRenderer( if (typeof item !== 'object' || item === null || !('str' in item)) continue const { str, transform, height } = item as PdfTextItem if (!str.trim()) continue - const [a, b, , , e, f] = transform + const [a, b, , , e, f] = transform as [number, number, number, number, number, number] const span = document.createElement('span') span.textContent = str span.style.cssText = diff --git a/client/src/features/staging/components/StagingFileSheet.vue b/client/src/features/staging/components/StagingFileSheet.vue index d138fb74c..18983a538 100644 --- a/client/src/features/staging/components/StagingFileSheet.vue +++ b/client/src/features/staging/components/StagingFileSheet.vue @@ -580,11 +580,13 @@ onMounted(() => {
diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index 70d16cb01..1485529c3 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -30,11 +30,13 @@ async function attemptRefresh(): Promise { } export async function api(input: RequestInfo | URL, init?: RequestInit & { _isRetry?: boolean }): Promise { - const res = await rawFetch(input, init) + const isRetry = init?._isRetry + const { _isRetry: _, ...rest } = init ?? {} + const res = await rawFetch(input, rest) if (res.status !== 401) return res - if (init?._isRetry) { + if (isRetry) { _onAuthFailure?.() throw new Error('Session expired') } @@ -52,5 +54,5 @@ export async function api(input: RequestInfo | URL, init?: RequestInit & { _isRe throw new Error('Session expired') } - return rawFetch(input, { ...init, _isRetry: true }) + return api(input, { ...rest, _isRetry: true }) } diff --git a/client/typecheck-baseline.txt b/client/typecheck-baseline.txt deleted file mode 100644 index 9c062eb53..000000000 --- a/client/typecheck-baseline.txt +++ /dev/null @@ -1,53 +0,0 @@ -# Client typecheck baseline -# Auto-generated by: pnpm run typecheck:client:baseline:update -# One diagnostic per line. Do not edit manually unless necessary. -src/features/book/components/detail/tabs/EditMetadataTab.vue(394,14): error TS2345: Argument of type '{ modelValue: string[]; }' is not assignable to parameter of type '{ readonly modelValue: string[]; readonly placeholder?: string | undefined; readonly searchFn: (q: string) => Promise; readonly "onUpdate:modelValue"?: ((args_0: string[]) => any) | undefined; } & VNodeProps & AllowedComponentProps & ComponentCustomProps & Record<...>'. -src/features/book/components/detail/tabs/EditMetadataTab.vue(400,16): error TS2345: Argument of type '{ modelValue: string[]; }' is not assignable to parameter of type '{ readonly modelValue: string[]; readonly placeholder?: string | undefined; readonly searchFn: (q: string) => Promise; readonly "onUpdate:modelValue"?: ((args_0: string[]) => any) | undefined; } & VNodeProps & AllowedComponentProps & ComponentCustomProps & Record<...>'. -src/features/book/components/detail/tabs/EditMetadataTab.vue(404,16): error TS2345: Argument of type '{ modelValue: string[]; }' is not assignable to parameter of type '{ readonly modelValue: string[]; readonly placeholder?: string | undefined; readonly searchFn: (q: string) => Promise; readonly "onUpdate:modelValue"?: ((args_0: string[]) => any) | undefined; } & VNodeProps & AllowedComponentProps & ComponentCustomProps & Record<...>'. -src/features/book/components/detail/tabs/EditMetadataTab.vue(411,16): error TS2345: Argument of type '{ modelValue: string[]; }' is not assignable to parameter of type '{ readonly modelValue: string[]; readonly placeholder?: string | undefined; readonly searchFn: (q: string) => Promise; readonly "onUpdate:modelValue"?: ((args_0: string[]) => any) | undefined; } & VNodeProps & AllowedComponentProps & ComponentCustomProps & Record<...>'. -src/features/book/components/detail/tabs/EditMetadataTab.vue(415,16): error TS2345: Argument of type '{ modelValue: string[]; }' is not assignable to parameter of type '{ readonly modelValue: string[]; readonly placeholder?: string | undefined; readonly searchFn: (q: string) => Promise; readonly "onUpdate:modelValue"?: ((args_0: string[]) => any) | undefined; } & VNodeProps & AllowedComponentProps & ComponentCustomProps & Record<...>'. -src/features/book/components/detail/tabs/EditMetadataTab.vue(421,14): error TS2345: Argument of type '{ modelValue: string[]; }' is not assignable to parameter of type '{ readonly modelValue: string[]; readonly placeholder?: string | undefined; readonly searchFn: (q: string) => Promise; readonly "onUpdate:modelValue"?: ((args_0: string[]) => any) | undefined; } & VNodeProps & AllowedComponentProps & ComponentCustomProps & Record<...>'. -src/features/book/components/detail/tabs/EditMetadataTab.vue(427,16): error TS2345: Argument of type '{ modelValue: string[]; }' is not assignable to parameter of type '{ readonly modelValue: string[]; readonly placeholder?: string | undefined; readonly searchFn: (q: string) => Promise; readonly "onUpdate:modelValue"?: ((args_0: string[]) => any) | undefined; } & VNodeProps & AllowedComponentProps & ComponentCustomProps & Record<...>'. -src/features/book/components/detail/tabs/EditMetadataTab.vue(431,16): error TS2345: Argument of type '{ modelValue: string[]; }' is not assignable to parameter of type '{ readonly modelValue: string[]; readonly placeholder?: string | undefined; readonly searchFn: (q: string) => Promise; readonly "onUpdate:modelValue"?: ((args_0: string[]) => any) | undefined; } & VNodeProps & AllowedComponentProps & ComponentCustomProps & Record<...>'. -src/features/book/components/detail/tabs/EditMetadataTab.vue(437,14): error TS2345: Argument of type '{ modelValue: string[]; }' is not assignable to parameter of type '{ readonly modelValue: string[]; readonly placeholder?: string | undefined; readonly searchFn: (q: string) => Promise; readonly "onUpdate:modelValue"?: ((args_0: string[]) => any) | undefined; } & VNodeProps & AllowedComponentProps & ComponentCustomProps & Record<...>'. -src/features/book/components/detail/tabs/MetadataDiffPanel.vue(115,20): error TS18048: '__VLS_ctx.activeCandidate' is possibly 'undefined'. -src/features/book/components/detail/tabs/MetadataDiffPanel.vue(117,19): error TS18048: '__VLS_ctx.activeCandidate' is possibly 'undefined'. -src/features/book/components/detail/tabs/MetadataDiffPanel.vue(124,69): error TS18048: '__VLS_ctx.activeCandidate' is possibly 'undefined'. -src/features/book/components/detail/tabs/MetadataDiffPanel.vue(126,105): error TS18048: '__VLS_ctx.activeCandidate' is possibly 'undefined'. -src/features/book/components/detail/tabs/MetadataDiffPanel.vue(127,35): error TS18048: '__VLS_ctx.activeCandidate' is possibly 'undefined'. -src/features/book/composables/__tests__/useMetadataDiff.spec.ts(107,90): error TS2345: Argument of type '{ key: MetadataProviderKey; label: string; }[]' is not assignable to parameter of type 'MaybeRefOrGetter'. -src/features/book/composables/__tests__/useMetadataDiff.spec.ts(117,140): error TS2345: Argument of type '{ key: MetadataProviderKey; label: string; }[]' is not assignable to parameter of type 'MaybeRefOrGetter'. -src/features/book/composables/__tests__/useMetadataDiff.spec.ts(131,99): error TS2345: Argument of type '{ key: MetadataProviderKey; label: string; }[]' is not assignable to parameter of type 'MaybeRefOrGetter'. -src/features/book/composables/__tests__/useMetadataDiff.spec.ts(151,81): error TS2345: Argument of type '{ key: MetadataProviderKey; label: string; }[]' is not assignable to parameter of type 'MaybeRefOrGetter'. -src/features/book/composables/__tests__/useMetadataDiff.spec.ts(161,81): error TS2345: Argument of type '{ key: MetadataProviderKey; label: string; }[]' is not assignable to parameter of type 'MaybeRefOrGetter'. -src/features/book/composables/__tests__/useMetadataDiff.spec.ts(47,81): error TS2345: Argument of type '{ key: MetadataProviderKey; label: string; }[]' is not assignable to parameter of type 'MaybeRefOrGetter'. -src/features/book/composables/__tests__/useMetadataDiff.spec.ts(58,81): error TS2345: Argument of type '{ key: MetadataProviderKey; label: string; }[]' is not assignable to parameter of type 'MaybeRefOrGetter'. -src/features/book/composables/__tests__/useMetadataDiff.spec.ts(68,94): error TS2345: Argument of type '{ key: MetadataProviderKey; label: string; }[]' is not assignable to parameter of type 'MaybeRefOrGetter'. -src/features/book/composables/__tests__/useMetadataDiff.spec.ts(7,9): error TS2739: Type '{ title: string; authors: string[]; genres: string[]; description: string; publisher: string; publishedYear: number; language: string; pageCount: number; seriesName: string; seriesIndex: number; isbn10: string; isbn13: string; }' is missing the following properties from type 'MetadataSource': subtitle, narrators, durationSeconds, abridged -src/features/book/composables/__tests__/useMetadataDiff.spec.ts(79,104): error TS2345: Argument of type '{ key: MetadataProviderKey; label: string; }[]' is not assignable to parameter of type 'MaybeRefOrGetter'. -src/features/book/composables/__tests__/useMetadataDiff.spec.ts(91,121): error TS2345: Argument of type '{ key: MetadataProviderKey; label: string; }[]' is not assignable to parameter of type 'MaybeRefOrGetter'. -src/features/dashboard/components/DashboardSettingsSheet.vue(45,26): error TS2345: Argument of type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; } | undefined' is not assignable to parameter of type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. -src/features/reader/epub/composables/useFoliateInput.ts(31,19): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(32,19): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(43,29): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(44,29): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(71,22): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(72,31): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(87,45): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(92,39): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(95,27): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useReaderState.ts(212,47): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'. -src/features/reader/epub/composables/useReaderState.ts(69,27): error TS18048: 'theme' is possibly 'undefined'. -src/features/reader/epub/composables/useReaderState.ts(69,40): error TS18048: 'theme' is possibly 'undefined'. -src/features/reader/epub/composables/useReaderState.ts(76,23): error TS18048: 'theme' is possibly 'undefined'. -src/features/reader/epub/composables/useReaderState.ts(77,22): error TS18048: 'theme' is possibly 'undefined'. -src/features/reader/epub/utils.ts(2,3): error TS2322: Type 'string | undefined' is not assignable to type 'string'. -src/features/reader/pdf/composables/usePdfFind.ts(33,22): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. -src/features/reader/pdf/composables/usePdfFind.ts(33,43): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. -src/features/reader/pdf/composables/usePdfRenderer.ts(97,17): error TS18048: 'e' is possibly 'undefined'. -src/features/reader/pdf/composables/usePdfRenderer.ts(98,34): error TS18048: 'f' is possibly 'undefined'. -src/features/reader/pdf/composables/usePdfRenderer.ts(99,31): error TS18048: 'a' is possibly 'undefined'. -src/features/reader/pdf/composables/usePdfRenderer.ts(99,50): error TS18048: 'b' is possibly 'undefined'. -src/features/settings/metadata-preferences/components/ProviderChipList.vue(56,34): error TS2345: Argument of type 'MetadataProviderKey | undefined' is not assignable to parameter of type 'MetadataProviderKey'. -src/features/staging/components/StagingFileSheet.vue(581,12): error TS2345: Argument of type '{ current: MetadataSource; candidate: { provider: MetadataProviderKey; providerId: string; title: string; subtitle?: string | undefined; authors?: string[] | undefined; description?: string | undefined; publisher?: string | undefined; ... 15 more ...; comicMetadata?: { ...; } | undefined; }; ... 5 more ...; onBack: ...' is not assignable to parameter of type '{ readonly current: MetadataSource; readonly candidates: MetadataCandidate[]; readonly initialCandidate: MetadataCandidate; ... 6 more ...; readonly onApply?: ((args_0: { ...; }) => any) | undefined; } & VNodeProps & AllowedComponentProps & ComponentCustomProps & Record<...>'. -src/lib/api.ts(55,37): error TS2353: Object literal may only specify known properties, and '_isRetry' does not exist in type 'RequestInit'. diff --git a/client/typecheck_output.txt b/client/typecheck_output.txt deleted file mode 100644 index c96553a80..000000000 --- a/client/typecheck_output.txt +++ /dev/null @@ -1,161 +0,0 @@ - -> client@0.0.0 type-check /Users/developer/Workspace/projectx/client -> vue-tsc --build - -src/features/book/components/detail/BookDetailHeader.vue(22,5): error TS2322: Type 'number | null | undefined' is not assignable to type 'number | null'. - Type 'undefined' is not assignable to type 'number | null'. -src/features/book/components/detail/BookDetailHeader.vue(23,5): error TS2322: Type 'number | null | undefined' is not assignable to type 'number | null'. - Type 'undefined' is not assignable to type 'number | null'. -src/features/dashboard/components/DashboardSettingsSheet.vue(45,26): error TS2345: Argument of type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; } | undefined' is not assignable to parameter of type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. - Type 'undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. -src/features/dashboard/components/DashboardSettingsSheet.vue(80,5): error TS2322: Type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; } | undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. - Type 'undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. -src/features/dashboard/components/DashboardSettingsSheet.vue(80,23): error TS2322: Type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; } | undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. - Type 'undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. -src/features/dashboard/components/DashboardSettingsSheet.vue(87,5): error TS2322: Type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; } | undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. - Type 'undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. -src/features/dashboard/components/DashboardSettingsSheet.vue(87,19): error TS2322: Type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; } | undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. - Type 'undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. -src/features/library/components/LibraryCreatorScanner.vue(36,36): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. - Type 'undefined' is not assignable to type 'string'. -src/features/library/composables/useBookUpload.ts(114,5): error TS2304: Cannot find name 'processQueue'. -src/features/reader/cbz/CbzReaderView.vue(95,17): error TS2532: Object is possibly 'undefined'. -src/features/reader/cbz/CbzReaderView.vue(99,14): error TS2532: Object is possibly 'undefined'. -src/features/reader/epub/components/NoteDialog.vue(14,25): error TS2345: Argument of type '"update:modelValue"' is not assignable to parameter of type '"save"'. -src/features/reader/epub/components/ReaderSidebar.vue(7,10): error TS2300: Duplicate identifier 'stripFragment'. -src/features/reader/epub/components/ReaderSidebar.vue(128,10): error TS2300: Duplicate identifier 'stripFragment'. -src/features/reader/epub/components/ReaderSidebar.vue(130,7): error TS7022: 'TocList' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer. -src/features/reader/epub/components/ReaderSidebar.vue(144,12): error TS7024: Function implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions. -src/features/reader/epub/components/ReaderSidebar.vue(148,25): error TS7024: Function implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions. -src/features/reader/epub/composables/useFoliate.ts(112,36): error TS2352: Conversion of type 'Window & typeof globalThis' to type 'Record' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. - Index signature for type 'string' is missing in type 'Window & typeof globalThis'. -src/features/reader/epub/composables/useFoliateAnnotations.ts(81,48): error TS2349: This expression is not callable. - Type '{}' has no call signatures. -src/features/reader/epub/composables/useFoliateAnnotations.ts(87,50): error TS2349: This expression is not callable. - Type '{}' has no call signatures. -src/features/reader/epub/composables/useFoliateAnnotations.ts(93,48): error TS2349: This expression is not callable. - Type '{}' has no call signatures. -src/features/reader/epub/composables/useFoliateAnnotations.ts(99,52): error TS2349: This expression is not callable. - Type '{}' has no call signatures. -src/features/reader/epub/composables/useFoliateInput.ts(31,19): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(32,19): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(43,29): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(44,29): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(71,22): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(72,31): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(87,45): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(92,39): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(95,27): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useReaderState.ts(69,27): error TS18048: 'theme' is possibly 'undefined'. -src/features/reader/epub/composables/useReaderState.ts(69,40): error TS18048: 'theme' is possibly 'undefined'. -src/features/reader/epub/composables/useReaderState.ts(76,23): error TS18048: 'theme' is possibly 'undefined'. -src/features/reader/epub/composables/useReaderState.ts(77,22): error TS18048: 'theme' is possibly 'undefined'. -src/features/reader/epub/composables/useReaderState.ts(212,47): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'. -src/features/reader/epub/utils.ts(2,3): error TS2322: Type 'string | undefined' is not assignable to type 'string'. - Type 'undefined' is not assignable to type 'string'. -src/features/reader/pdf/components/PdfToolbar.vue(205,93): error TS2769: No overload matches this call. - The last overload gave the following error. - Argument of type '"applyZoomPreset"' is not assignable to parameter of type '"update:cursorTool"'. -src/features/reader/pdf/composables/usePdfFind.ts(33,22): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. - Type 'undefined' is not assignable to type 'string'. -src/features/reader/pdf/composables/usePdfFind.ts(33,43): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. - Type 'undefined' is not assignable to type 'string'. -src/features/reader/pdf/composables/usePdfRenderer.ts(97,17): error TS18048: 'e' is possibly 'undefined'. -src/features/reader/pdf/composables/usePdfRenderer.ts(98,34): error TS18048: 'f' is possibly 'undefined'. -src/features/reader/pdf/composables/usePdfRenderer.ts(99,31): error TS18048: 'a' is possibly 'undefined'. -src/features/reader/pdf/composables/usePdfRenderer.ts(99,50): error TS18048: 'b' is possibly 'undefined'. -src/features/reader/ReaderView.vue(149,15): error TS2345: Argument of type 'unknown[]' is not assignable to parameter of type 'RawTocItem[]'. - Type 'unknown' is not assignable to type 'RawTocItem'. -src/features/reader/ReaderView.vue(241,12): error TS2345: Argument of type '{}' is not assignable to parameter of type 'FoliateView'. - Property 'search' is missing in type '{}' but required in type 'FoliateView'. -src/features/reader/ReaderView.vue(253,15): error TS2345: Argument of type 'unknown' is not assignable to parameter of type 'FoliateView | null'. -src/features/settings/metadata-preferences/components/FieldConfigSheet.vue(57,5): error TS2322: Type 'MetadataProviderKey | undefined' is not assignable to type 'MetadataProviderKey'. - Type 'undefined' is not assignable to type 'MetadataProviderKey'. -src/features/settings/metadata-preferences/components/FieldConfigSheet.vue(57,25): error TS2322: Type 'MetadataProviderKey | undefined' is not assignable to type 'MetadataProviderKey'. - Type 'undefined' is not assignable to type 'MetadataProviderKey'. -src/features/settings/metadata-preferences/components/FieldConfigSheet.vue(65,5): error TS2322: Type 'MetadataProviderKey | undefined' is not assignable to type 'MetadataProviderKey'. - Type 'undefined' is not assignable to type 'MetadataProviderKey'. -src/features/settings/metadata-preferences/components/FieldConfigSheet.vue(65,21): error TS2322: Type 'MetadataProviderKey | undefined' is not assignable to type 'MetadataProviderKey'. - Type 'undefined' is not assignable to type 'MetadataProviderKey'. -src/features/settings/metadata-preferences/components/ProviderChipList.vue(56,34): error TS2345: Argument of type 'MetadataProviderKey | undefined' is not assignable to parameter of type 'MetadataProviderKey'. - Type 'undefined' is not assignable to type 'MetadataProviderKey'. -src/lib/api.ts(55,37): error TS2353: Object literal may only specify known properties, and '_isRetry' does not exist in type 'RequestInit'. -src/features/book/components/detail/BookDetailHeader.vue(22,5): error TS2322: Type 'number | null | undefined' is not assignable to type 'number | null'. - Type 'undefined' is not assignable to type 'number | null'. -src/features/book/components/detail/BookDetailHeader.vue(23,5): error TS2322: Type 'number | null | undefined' is not assignable to type 'number | null'. - Type 'undefined' is not assignable to type 'number | null'. -src/features/dashboard/components/DashboardSettingsSheet.vue(45,26): error TS2345: Argument of type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; } | undefined' is not assignable to parameter of type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. - Type 'undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. -src/features/dashboard/components/DashboardSettingsSheet.vue(80,5): error TS2322: Type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; } | undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. - Type 'undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. -src/features/dashboard/components/DashboardSettingsSheet.vue(80,23): error TS2322: Type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; } | undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. - Type 'undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. -src/features/dashboard/components/DashboardSettingsSheet.vue(87,5): error TS2322: Type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; } | undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. - Type 'undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. -src/features/dashboard/components/DashboardSettingsSheet.vue(87,19): error TS2322: Type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; } | undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. - Type 'undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'. -src/features/library/components/LibraryCreatorScanner.vue(36,36): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. - Type 'undefined' is not assignable to type 'string'. -src/features/library/composables/useBookUpload.ts(114,5): error TS2304: Cannot find name 'processQueue'. -src/features/reader/cbz/CbzReaderView.vue(95,17): error TS2532: Object is possibly 'undefined'. -src/features/reader/cbz/CbzReaderView.vue(99,14): error TS2532: Object is possibly 'undefined'. -src/features/reader/epub/components/NoteDialog.vue(14,25): error TS2345: Argument of type '"update:modelValue"' is not assignable to parameter of type '"save"'. -src/features/reader/epub/components/ReaderSidebar.vue(7,10): error TS2300: Duplicate identifier 'stripFragment'. -src/features/reader/epub/components/ReaderSidebar.vue(128,10): error TS2300: Duplicate identifier 'stripFragment'. -src/features/reader/epub/components/ReaderSidebar.vue(130,7): error TS7022: 'TocList' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer. -src/features/reader/epub/components/ReaderSidebar.vue(144,12): error TS7024: Function implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions. -src/features/reader/epub/components/ReaderSidebar.vue(148,25): error TS7024: Function implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions. -src/features/reader/epub/composables/useFoliate.ts(112,36): error TS2352: Conversion of type 'Window & typeof globalThis' to type 'Record' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. - Index signature for type 'string' is missing in type 'Window & typeof globalThis'. -src/features/reader/epub/composables/useFoliateAnnotations.ts(81,48): error TS2349: This expression is not callable. - Type '{}' has no call signatures. -src/features/reader/epub/composables/useFoliateAnnotations.ts(87,50): error TS2349: This expression is not callable. - Type '{}' has no call signatures. -src/features/reader/epub/composables/useFoliateAnnotations.ts(93,48): error TS2349: This expression is not callable. - Type '{}' has no call signatures. -src/features/reader/epub/composables/useFoliateAnnotations.ts(99,52): error TS2349: This expression is not callable. - Type '{}' has no call signatures. -src/features/reader/epub/composables/useFoliateInput.ts(31,19): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(32,19): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(43,29): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(44,29): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(71,22): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(72,31): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(87,45): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(92,39): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useFoliateInput.ts(95,27): error TS18048: 'touch' is possibly 'undefined'. -src/features/reader/epub/composables/useReaderState.ts(69,27): error TS18048: 'theme' is possibly 'undefined'. -src/features/reader/epub/composables/useReaderState.ts(69,40): error TS18048: 'theme' is possibly 'undefined'. -src/features/reader/epub/composables/useReaderState.ts(76,23): error TS18048: 'theme' is possibly 'undefined'. -src/features/reader/epub/composables/useReaderState.ts(77,22): error TS18048: 'theme' is possibly 'undefined'. -src/features/reader/epub/composables/useReaderState.ts(212,47): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'. -src/features/reader/epub/utils.ts(2,3): error TS2322: Type 'string | undefined' is not assignable to type 'string'. - Type 'undefined' is not assignable to type 'string'. -src/features/reader/pdf/components/PdfToolbar.vue(205,93): error TS2769: No overload matches this call. - The last overload gave the following error. - Argument of type '"applyZoomPreset"' is not assignable to parameter of type '"update:cursorTool"'. -src/features/reader/pdf/composables/usePdfFind.ts(33,22): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. - Type 'undefined' is not assignable to type 'string'. -src/features/reader/pdf/composables/usePdfFind.ts(33,43): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. - Type 'undefined' is not assignable to type 'string'. -src/features/reader/pdf/composables/usePdfRenderer.ts(97,17): error TS18048: 'e' is possibly 'undefined'. -src/features/reader/pdf/composables/usePdfRenderer.ts(98,34): error TS18048: 'f' is possibly 'undefined'. -src/features/reader/pdf/composables/usePdfRenderer.ts(99,31): error TS18048: 'a' is possibly 'undefined'. -src/features/reader/pdf/composables/usePdfRenderer.ts(99,50): error TS18048: 'b' is possibly 'undefined'. -src/features/reader/ReaderView.vue(149,15): error TS2345: Argument of type 'unknown[]' is not assignable to parameter of type 'RawTocItem[]'. - Type 'unknown' is not assignable to type 'RawTocItem'. -src/features/reader/ReaderView.vue(241,12): error TS2345: Argument of type '{}' is not assignable to parameter of type 'FoliateView'. - Property 'search' is missing in type '{}' but required in type 'FoliateView'. -src/features/reader/ReaderView.vue(253,15): error TS2345: Argument of type 'unknown' is not assignable to parameter of type 'FoliateView | null'. -src/features/settings/metadata-preferences/components/FieldConfigSheet.vue(57,5): error TS2322: Type 'MetadataProviderKey | undefined' is not assignable to type 'MetadataProviderKey'. - Type 'undefined' is not assignable to type 'MetadataProviderKey'. -src/features/settings/metadata-preferences/components/FieldConfigSheet.vue(57,25): error TS2322: Type 'MetadataProviderKey | undefined' is not assignable to type 'MetadataProviderKey'. - Type 'undefined' is not assignable to type 'MetadataProviderKey'. -src/features/settings/metadata-preferences/components/FieldConfigSheet.vue(65,5): error TS2322: Type 'MetadataProviderKey | undefined' is not assignable to type 'MetadataProviderKey'. - Type 'undefined' is not assignable to type 'MetadataProviderKey'. -src/features/settings/metadata-preferences/components/FieldConfigSheet.vue(65,21): error TS2322: Type 'MetadataProviderKey | undefined' is not assignable to type 'MetadataProviderKey'. - Type 'undefined' is not assignable to type 'MetadataProviderKey'. -src/features/settings/metadata-preferences/components/ProviderChipList.vue(56,34): error TS2345: Argument of type 'MetadataProviderKey | undefined' is not assignable to parameter of type 'MetadataProviderKey'. - Type 'undefined' is not assignable to type 'MetadataProviderKey'. -src/lib/api.ts(55,37): error TS2353: Object literal may only specify known properties, and '_isRetry' does not exist in type 'RequestInit'. - ELIFECYCLE  Command failed with exit code 2. diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 000000000..6d2605465 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,24 @@ +name: projectx-dev + +services: + postgres: + image: pgvector/pgvector:pg16 + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-projectx} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-projectx} + POSTGRES_DB: ${POSTGRES_DB:-projectx} + POSTGRES_HOST_AUTH_METHOD: trust + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U projectx"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + postgres_data: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml deleted file mode 100644 index afc24e6e5..000000000 --- a/docker-compose.prod.yml +++ /dev/null @@ -1,106 +0,0 @@ -name: projectx - -x-logging: &default-logging - driver: json-file - options: - max-size: "10m" - max-file: "5" - -services: - postgres: - image: pgvector/pgvector:pg16 - restart: unless-stopped - environment: - POSTGRES_USER: ${POSTGRES_USER?POSTGRES_USER is required} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD?POSTGRES_PASSWORD is required} - POSTGRES_DB: ${POSTGRES_DB?POSTGRES_DB is required} - volumes: - - postgres_data:/var/lib/postgresql/data - - ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro - healthcheck: - test: - [ - "CMD-SHELL", - "pg_isready -U ${POSTGRES_USER?POSTGRES_USER is required} -d ${POSTGRES_DB?POSTGRES_DB is required}", - ] - interval: 10s - timeout: 5s - retries: 10 - start_period: 20s - logging: *default-logging - - migrate: - image: ${APP_IMAGE?APP_IMAGE is required} - restart: "no" - init: true - environment: - DATABASE_URL: postgres://${POSTGRES_USER?POSTGRES_USER is required}:${POSTGRES_PASSWORD?POSTGRES_PASSWORD is required}@postgres:5432/${POSTGRES_DB?POSTGRES_DB is required} - NODE_ENV: production - depends_on: - postgres: - condition: service_healthy - command: ["node", "dist/scripts/migrate.js"] - read_only: true - tmpfs: - - /tmp - cap_drop: - - ALL - security_opt: - - no-new-privileges:true - logging: *default-logging - - app: - image: ${APP_IMAGE?APP_IMAGE is required} - restart: unless-stopped - init: true - ports: - - "${APP_PORT:-3000}:3000" - environment: - DATABASE_URL: postgres://${POSTGRES_USER?POSTGRES_USER is required}:${POSTGRES_PASSWORD?POSTGRES_PASSWORD is required}@postgres:5432/${POSTGRES_DB?POSTGRES_DB is required} - JWT_SECRET: ${JWT_SECRET?JWT_SECRET is required} - APP_URL: ${APP_URL?APP_URL is required} - NODE_ENV: production - BOOKS_PATH: /books - STAGING_PATH: /books/staging - PORT: 3000 - JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-15m} - JWT_REFRESH_EXPIRES_IN: ${JWT_REFRESH_EXPIRES_IN:-7d} - CLIENT_URL: ${CLIENT_URL:-http://localhost:5173} - SMTP_HOST: ${SMTP_HOST:-} - SMTP_PORT: ${SMTP_PORT:-587} - SMTP_SECURE: ${SMTP_SECURE:-false} - SMTP_USER: ${SMTP_USER:-} - SMTP_PASS: ${SMTP_PASS:-} - SMTP_FROM: ${SMTP_FROM:-noreply@projectx.local} - EMAIL_ENCRYPTION_KEY: ${EMAIL_ENCRYPTION_KEY:-} - GOOGLE_BOOKS_API_KEY: ${GOOGLE_BOOKS_API_KEY:-} - depends_on: - postgres: - condition: service_healthy - migrate: - condition: service_completed_successfully - volumes: - - books_data:/books - read_only: true - tmpfs: - - /tmp - cap_drop: - - ALL - security_opt: - - no-new-privileges:true - stop_grace_period: 30s - healthcheck: - test: - [ - "CMD-SHELL", - 'node -e "fetch(''http://127.0.0.1:3000/api/v1/health'').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"', - ] - interval: 30s - timeout: 5s - retries: 3 - start_period: 20s - logging: *default-logging - -volumes: - postgres_data: - books_data: diff --git a/docker-compose.yml b/docker-compose.yml index d054a178f..e5c5b5a16 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,25 +1,61 @@ -name: projectx-dev +name: projectx services: postgres: - image: pgvector/pgvector:pg16 + image: pgvector/pgvector:pg16@sha256:7d400e340efb42f4d8c9c12c6427adb253f726881a9985d2a471bf0eed824dff restart: unless-stopped environment: - POSTGRES_USER: ${POSTGRES_USER:-projectx} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-projectx} - POSTGRES_DB: ${POSTGRES_DB:-projectx} - POSTGRES_HOST_AUTH_METHOD: trust - ports: - - "5432:5432" + POSTGRES_USER: ${POSTGRES_USER?required} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD?required} + POSTGRES_DB: ${POSTGRES_DB?required} volumes: - - postgres_data:/var/lib/postgresql/data - - ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql + - ./data/postgres:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U projectx"] - interval: 5s + test: + [ + "CMD-SHELL", + "pg_isready -U ${POSTGRES_USER?required} -d ${POSTGRES_DB?required}", + ] + interval: 10s timeout: 5s - retries: 5 + retries: 10 + start_period: 20s - -volumes: - postgres_data: + app: + image: ${APP_IMAGE?required} + restart: unless-stopped + init: true + ports: + - "${APP_PORT:-3000}:3000" + environment: + POSTGRES_USER: ${POSTGRES_USER?required} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD?required} + POSTGRES_DB: ${POSTGRES_DB?required} + JWT_SECRET: ${JWT_SECRET?required} + SETUP_BOOTSTRAP_TOKEN: ${SETUP_BOOTSTRAP_TOKEN?required} + APP_URL: ${APP_URL?required} + depends_on: + postgres: + condition: service_healthy + volumes: + - ./books:/books + - ./staging:/staging + - ./data:/data + read_only: true + tmpfs: + - /tmp + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + stop_grace_period: 30s + healthcheck: + test: + [ + "CMD-SHELL", + 'node -e "fetch(''http://127.0.0.1:''+(process.env.PORT||3000)+''/api/v1/health'').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"', + ] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s diff --git a/docs/production.md b/docs/production.md deleted file mode 100644 index 690c493da..000000000 --- a/docs/production.md +++ /dev/null @@ -1,84 +0,0 @@ -# Production Deployment (Docker Compose) - -This project's production Compose stack is intentionally separate from local development. - -## 1) Configure production environment - -Create `.env.prod` from the example and set real values: - -```bash -cp .env.prod.example .env.prod -``` - -Required fields: - -- `APP_IMAGE` (immutable tag or digest from CI, for example `ghcr.io//:sha-`) -- `POSTGRES_USER` -- `POSTGRES_PASSWORD` -- `POSTGRES_DB` -- `JWT_SECRET` -- `APP_URL` - -## 2) Deploy - -```bash -docker compose --env-file .env.prod -f docker-compose.prod.yml pull -docker compose --env-file .env.prod -f docker-compose.prod.yml up -d --wait -``` - -What happens: - -- `postgres` starts with `pgvector` enabled via init SQL. -- `migrate` runs once (`node dist/scripts/migrate.js`) and must complete successfully. -- `app` starts only after DB health is `healthy` and migration service exits successfully. - -## 3) Roll forward - -1. Update `APP_IMAGE` in `.env.prod` to the new immutable image. -2. Pull + restart: - -```bash -docker compose --env-file .env.prod -f docker-compose.prod.yml pull -docker compose --env-file .env.prod -f docker-compose.prod.yml up -d --wait -``` - -## 4) Backup strategy - -Create a compressed custom-format Postgres backup: - -```bash -pnpm db:backup:prod -``` - -By default this writes to `local/backups/`. - -Recommended cadence: - -- Daily backups -- Keep at least 7 daily + 4 weekly + 3 monthly snapshots -- Store a copy off-host - -## 5) Restore strategy - -Restore into the live DB: - -```bash -pnpm db:restore:prod -- local/backups/.dump -``` - -Run a non-destructive restore test into a temporary DB: - -```bash -pnpm db:restore:test:prod -- local/backups/.dump -``` - -Run the restore test at least monthly so backup integrity is continuously verified. - -## 6) CI image pipeline - -Workflow: `.github/workflows/container-image.yml` - -On `main` and tag pushes it: - -- builds the Docker image -- pushes immutable tags to GHCR (`sha-*`) diff --git a/package.json b/package.json index cf64b16b9..3f4b1ebb4 100644 --- a/package.json +++ b/package.json @@ -2,26 +2,19 @@ "name": "projectx", "private": true, "scripts": { - "help": "node ./scripts/help.mjs", - "guide": "node ./scripts/help.mjs", "setup": "bash ./scripts/setup.sh", - "doctor": "bash ./scripts/doctor.sh", "dev": "concurrently --names \"types,server,client\" --prefix-colors \"cyan,blue,green\" \"pnpm --filter @projectx/types dev\" \"npm run dev:server\" \"npm run dev:client\"", "dev:client": "cd client && npm run dev -- --host", "dev:server": "cd server && npm run start:dev", - "quick": "pnpm run verify:fast", "verify:fast": "pnpm run lint:check && pnpm run typecheck && pnpm run test", "verify": "pnpm run lint:check && pnpm run typecheck && pnpm run test && pnpm run test:e2e:smoke", "verify:strict": "pnpm run format:check && pnpm run lint:check && pnpm run typecheck:full && pnpm run test && pnpm run test:e2e:smoke", "build:client": "pnpm --filter client build", "build:server": "pnpm --filter server build", - "typecheck": "pnpm run typecheck:server && pnpm run typecheck:client:baseline", + "typecheck": "pnpm run typecheck:server && pnpm run typecheck:client", "typecheck:full": "pnpm run typecheck:server && pnpm run typecheck:client", "typecheck:server": "pnpm --filter server type-check", "typecheck:client": "pnpm --filter client type-check", - "typecheck:client:baseline": "node ./scripts/client-typecheck-baseline.mjs check", - "typecheck:client:baseline:update": "node ./scripts/client-typecheck-baseline.mjs update", - "lint": "pnpm run lint:check", "lint:check": "pnpm --filter server lint:check && pnpm --filter client lint:check", "lint:fix": "pnpm --filter server lint:fix && pnpm --filter client lint:fix", "format": "pnpm --filter server format && pnpm --filter client format", @@ -33,12 +26,9 @@ "coverage": "pnpm run coverage:server && pnpm run coverage:client", "coverage:server": "pnpm --filter server test:cov", "coverage:client": "pnpm --filter client test:cov --run", - "db:up": "docker compose up -d --wait", - "prod:up": "docker compose --env-file .env.prod -f docker-compose.prod.yml up -d --wait", - "prod:down": "docker compose --env-file .env.prod -f docker-compose.prod.yml down", - "db:backup:prod": "bash ./scripts/db/backup-prod.sh .env.prod", - "db:restore:prod": "bash ./scripts/db/restore-prod.sh .env.prod", - "db:restore:test:prod": "bash ./scripts/db/restore-test-prod.sh .env.prod", + "db:up": "docker compose -f docker-compose.dev.yml up -d --wait", + "prod:up": "docker compose up -d --wait", + "prod:down": "docker compose down", "db:migrate": "pnpm --filter server db:migrate", "db:reset": "bash ./scripts/db/reset.sh", "db:seed": "bash ./scripts/db/seed.sh", diff --git a/packages/types/src/audit.ts b/packages/types/src/audit.ts index 1ca266d88..917574215 100644 --- a/packages/types/src/audit.ts +++ b/packages/types/src/audit.ts @@ -108,7 +108,6 @@ export interface AuditLogEntry { resourceId: number | null; description: string; ip: string | null; - countryCode: string | null; meta: Record | null; createdAt: string; } diff --git a/scripts/client-typecheck-baseline.mjs b/scripts/client-typecheck-baseline.mjs deleted file mode 100644 index 4020cbc8c..000000000 --- a/scripts/client-typecheck-baseline.mjs +++ /dev/null @@ -1,120 +0,0 @@ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' -import { dirname, resolve } from 'node:path' -import { spawnSync } from 'node:child_process' - -const mode = process.argv[2] ?? 'check' -const rootDir = process.cwd() -const baselinePath = resolve(rootDir, 'client/typecheck-baseline.txt') - -function runClientTypecheck() { - const result = spawnSync('pnpm', ['--filter', 'client', 'exec', 'vue-tsc', '--build', '--pretty', 'false'], { - cwd: rootDir, - encoding: 'utf8', - }) - - if (result.error) { - throw result.error - } - - const combinedOutput = `${result.stdout ?? ''}\n${result.stderr ?? ''}` - const diagnostics = parseDiagnostics(combinedOutput) - return { result, combinedOutput, diagnostics } -} - -function parseDiagnostics(output) { - const regex = /^(.*\.(?:ts|tsx|vue)\(\d+,\d+\): error TS\d+: .*)$/gm - const rows = [] - - for (const match of output.matchAll(regex)) { - const raw = String(match[1] ?? '').trim() - rows.push(normalizeDiagnostic(raw)) - } - - return [...new Set(rows)].sort((a, b) => a.localeCompare(b)) -} - -function normalizeDiagnostic(row) { - const rootPrefix = `${rootDir.replaceAll('\\', '/')}/` - let normalized = row.replaceAll('\\', '/') - normalized = normalized.split(`${rootPrefix}client/`).join('') - normalized = normalized.split(rootPrefix).join('') - if (normalized.startsWith('client/')) normalized = normalized.slice('client/'.length) - return normalized -} - -function readBaseline() { - if (!existsSync(baselinePath)) return [] - return readFileSync(baselinePath, 'utf8') - .split('\n') - .map((line) => line.trim()) - .filter((line) => line.length > 0 && !line.startsWith('#')) -} - -function writeBaseline(diagnostics) { - mkdirSync(dirname(baselinePath), { recursive: true }) - const header = [ - '# Client typecheck baseline', - '# Auto-generated by: pnpm run typecheck:client:baseline:update', - '# One diagnostic per line. Do not edit manually unless necessary.', - '', - ] - writeFileSync(baselinePath, `${header.join('\n')}${diagnostics.join('\n')}\n`, 'utf8') -} - -if (mode === 'update') { - const { result, combinedOutput, diagnostics } = runClientTypecheck() - if (result.status !== 0 && diagnostics.length === 0) { - console.error('Client typecheck failed without parsable diagnostics output.') - console.error(combinedOutput) - process.exit(1) - } - writeBaseline(diagnostics) - console.log(`Updated ${baselinePath} with ${diagnostics.length} diagnostics.`) - process.exit(0) -} - -if (mode !== 'check') { - console.error(`Unknown mode "${mode}". Use "check" or "update".`) - process.exit(2) -} - -const { result, combinedOutput, diagnostics } = runClientTypecheck() - -if (result.status === 0) { - console.log('Client typecheck is clean.') - process.exit(0) -} - -if (diagnostics.length === 0) { - console.error('Client typecheck failed without parsable diagnostics output.') - console.error(combinedOutput) - process.exit(1) -} - -const baseline = readBaseline() -if (baseline.length === 0) { - console.error(`No baseline found at ${baselinePath}.`) - console.error('Run: pnpm run typecheck:client:baseline:update') - process.exit(1) -} - -const baselineSet = new Set(baseline) -const newDiagnostics = diagnostics.filter((line) => !baselineSet.has(line)) - -if (newDiagnostics.length > 0) { - console.error(`Found ${newDiagnostics.length} new client type errors not present in baseline:`) - for (const line of newDiagnostics) { - console.error(`- ${line}`) - } - console.error('If these are intentional and accepted, refresh baseline:') - console.error('pnpm run typecheck:client:baseline:update') - process.exit(1) -} - -const resolvedCount = baseline.filter((line) => !diagnostics.includes(line)).length -console.log(`No new client type errors. Baseline size: ${baseline.length}. Current: ${diagnostics.length}. Resolved: ${resolvedCount}.`) -if (resolvedCount > 0) { - console.log('You can refresh baseline to capture improvements:') - console.log('pnpm run typecheck:client:baseline:update') -} -process.exit(0) diff --git a/scripts/db/backup-prod.sh b/scripts/db/backup-prod.sh deleted file mode 100755 index 50ab8bf53..000000000 --- a/scripts/db/backup-prod.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$ROOT_DIR" - -ENV_FILE="${1:-.env.prod}" -OUT_DIR="${2:-$ROOT_DIR/local/backups}" - -if [[ ! -f "$ENV_FILE" ]]; then - echo "Missing env file: $ENV_FILE" >&2 - exit 1 -fi - -set -a -# shellcheck disable=SC1090 -source "$ENV_FILE" -set +a - -: "${POSTGRES_USER:?POSTGRES_USER is required}" -: "${POSTGRES_DB:?POSTGRES_DB is required}" - -mkdir -p "$OUT_DIR" -timestamp="$(date -u +%Y%m%dT%H%M%SZ)" -backup_file="$OUT_DIR/projectx-${POSTGRES_DB}-${timestamp}.dump" - -docker compose --env-file "$ENV_FILE" -f docker-compose.prod.yml up -d --wait postgres -docker compose --env-file "$ENV_FILE" -f docker-compose.prod.yml exec -T postgres \ - pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc >"$backup_file" - -echo "Backup created: $backup_file" diff --git a/scripts/db/reset.sh b/scripts/db/reset.sh index 54c47e441..e07a29bb7 100644 --- a/scripts/db/reset.sh +++ b/scripts/db/reset.sh @@ -8,16 +8,16 @@ POSTGRES_USER="${POSTGRES_USER:-projectx}" POSTGRES_DB="${POSTGRES_DB:-projectx}" echo "Ensuring PostgreSQL is running..." -docker compose up -d --wait postgres +docker compose -f docker-compose.dev.yml up -d --wait postgres echo "Resetting database schema..." -docker compose exec -T postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" -docker compose exec -T postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";" -docker compose exec -T postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;" -docker compose exec -T postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE EXTENSION IF NOT EXISTS vector;" -docker compose exec -T postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE SCHEMA IF NOT EXISTS drizzle;" -docker compose exec -T postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE TABLE IF NOT EXISTS drizzle.__drizzle_migrations (id serial PRIMARY KEY, hash text NOT NULL, created_at bigint);" -docker compose exec -T postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "DELETE FROM drizzle.__drizzle_migrations;" +docker compose -f docker-compose.dev.yml exec -T postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" +docker compose -f docker-compose.dev.yml exec -T postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";" +docker compose -f docker-compose.dev.yml exec -T postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;" +docker compose -f docker-compose.dev.yml exec -T postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE EXTENSION IF NOT EXISTS vector;" +docker compose -f docker-compose.dev.yml exec -T postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE SCHEMA IF NOT EXISTS drizzle;" +docker compose -f docker-compose.dev.yml exec -T postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE TABLE IF NOT EXISTS drizzle.__drizzle_migrations (id serial PRIMARY KEY, hash text NOT NULL, created_at bigint);" +docker compose -f docker-compose.dev.yml exec -T postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "DELETE FROM drizzle.__drizzle_migrations;" echo "Re-applying migrations..." pnpm run db:migrate diff --git a/scripts/db/restore-prod.sh b/scripts/db/restore-prod.sh deleted file mode 100755 index 6024b074c..000000000 --- a/scripts/db/restore-prod.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$ROOT_DIR" - -ENV_FILE="${1:-.env.prod}" -BACKUP_FILE="${2:-}" - -if [[ ! -f "$ENV_FILE" ]]; then - echo "Missing env file: $ENV_FILE" >&2 - exit 1 -fi - -if [[ -z "$BACKUP_FILE" || ! -f "$BACKUP_FILE" ]]; then - echo "Usage: $0 " >&2 - exit 1 -fi - -set -a -# shellcheck disable=SC1090 -source "$ENV_FILE" -set +a - -: "${POSTGRES_USER:?POSTGRES_USER is required}" -: "${POSTGRES_DB:?POSTGRES_DB is required}" - -docker compose --env-file "$ENV_FILE" -f docker-compose.prod.yml up -d --wait postgres -docker compose --env-file "$ENV_FILE" -f docker-compose.prod.yml exec -T postgres \ - psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$POSTGRES_DB' AND pid <> pg_backend_pid();" >/dev/null - -cat "$BACKUP_FILE" | docker compose --env-file "$ENV_FILE" -f docker-compose.prod.yml exec -T postgres \ - pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --clean --if-exists --no-owner --no-privileges - -echo "Restore complete: $BACKUP_FILE -> $POSTGRES_DB" diff --git a/scripts/db/restore-test-prod.sh b/scripts/db/restore-test-prod.sh deleted file mode 100755 index 9848ec5ce..000000000 --- a/scripts/db/restore-test-prod.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$ROOT_DIR" - -ENV_FILE="${1:-.env.prod}" -BACKUP_FILE="${2:-}" - -if [[ ! -f "$ENV_FILE" ]]; then - echo "Missing env file: $ENV_FILE" >&2 - exit 1 -fi - -if [[ -z "$BACKUP_FILE" || ! -f "$BACKUP_FILE" ]]; then - echo "Usage: $0 " >&2 - exit 1 -fi - -set -a -# shellcheck disable=SC1090 -source "$ENV_FILE" -set +a - -: "${POSTGRES_USER:?POSTGRES_USER is required}" -: "${POSTGRES_DB:?POSTGRES_DB is required}" - -test_db="${POSTGRES_DB}_restore_test_$(date -u +%Y%m%d%H%M%S)" - -docker compose --env-file "$ENV_FILE" -f docker-compose.prod.yml up -d --wait postgres -docker compose --env-file "$ENV_FILE" -f docker-compose.prod.yml exec -T postgres \ - psql -U "$POSTGRES_USER" -d postgres -c "CREATE DATABASE \"$test_db\";" >/dev/null - -cleanup() { - docker compose --env-file "$ENV_FILE" -f docker-compose.prod.yml exec -T postgres \ - psql -U "$POSTGRES_USER" -d postgres -c "DROP DATABASE IF EXISTS \"$test_db\";" >/dev/null || true -} -trap cleanup EXIT - -cat "$BACKUP_FILE" | docker compose --env-file "$ENV_FILE" -f docker-compose.prod.yml exec -T postgres \ - pg_restore -U "$POSTGRES_USER" -d "$test_db" --no-owner --no-privileges >/dev/null - -table_count="$( - docker compose --env-file "$ENV_FILE" -f docker-compose.prod.yml exec -T postgres \ - psql -U "$POSTGRES_USER" -d "$test_db" -Atc "SELECT count(*) FROM information_schema.tables WHERE table_schema='public';" -)" - -echo "Restore test succeeded: $BACKUP_FILE -> $test_db (${table_count} public tables)" diff --git a/scripts/doctor.sh b/scripts/doctor.sh deleted file mode 100644 index 125e9809b..000000000 --- a/scripts/doctor.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$ROOT_DIR" - -echo "Checking toolchain..." -node -v -pnpm -v - -echo "Checking environment files..." -[[ -f server/.env.example ]] || { echo "Missing server/.env.example"; exit 1; } -[[ -f server/.env ]] || { echo "Missing server/.env (run: pnpm setup)"; exit 1; } - -echo "Checking PostgreSQL container..." -docker compose ps postgres - -POSTGRES_USER="${POSTGRES_USER:-projectx}" -POSTGRES_DB="${POSTGRES_DB:-projectx}" -docker compose exec -T postgres pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" - -echo "Checking local scripts..." -[[ -d scripts/db ]] || { echo "Missing scripts/db helpers"; exit 1; } -[[ -f scripts/help.mjs ]] || { echo "Missing scripts/help.mjs"; exit 1; } - -echo "Doctor check passed." diff --git a/scripts/help.mjs b/scripts/help.mjs deleted file mode 100644 index 3cf834d73..000000000 --- a/scripts/help.mjs +++ /dev/null @@ -1,24 +0,0 @@ -const lines = [ - 'ProjectX local workflow', - '', - 'Core commands', - ' pnpm setup # one-time bootstrap on a machine', - ' pnpm dev # run server + client for daily work', - ' pnpm verify # default local quality gate before push', - '', - 'Optional', - ' pnpm quick # faster local confidence loop', - ' pnpm verify:strict # aspirational strict gate (format + full typecheck)', - ' pnpm guide # print this command reference', - ' pnpm typecheck:client:baseline:update # refresh accepted client TS baseline', - ' pnpm doctor # environment health checks', - ' pnpm coverage # coverage reports for server + client', - '', - 'Database helpers', - ' pnpm db:up', - ' pnpm db:migrate', - ' pnpm db:reset', - ' pnpm db:seed', -]; - -process.stdout.write(`${lines.join('\n')}\n`); diff --git a/scripts/seed/reading-sessions.sh b/scripts/seed/reading-sessions.sh deleted file mode 100755 index 25c91ffae..000000000 --- a/scripts/seed/reading-sessions.sh +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env bash -# Seeds synthetic reading sessions and daily stats for the first user so that -# all user-statistics charts (heatmap, peak hours, completion timeline, etc.) -# show realistic data out of the box. -# -# Safe to run multiple times - sessions are deduplicated by session_id and -# daily stats are upserted. Running again adds more data on top. -# -# Usage: ./scripts/seed/reading-sessions.sh [USER_ID] -# USER_ID defaults to 1. - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$ROOT_DIR" - -POSTGRES_USER="${POSTGRES_USER:-projectx}" -POSTGRES_DB="${POSTGRES_DB:-projectx}" -TARGET_USER_ID="${1:-1}" - -echo "Seeding reading sessions for user_id=${TARGET_USER_ID}..." - -docker compose exec -T postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" << EOSQL - -DO \$\$ -DECLARE - v_user_id int := ${TARGET_USER_ID}; - v_day date; - v_file_id int; - v_library_id int; - v_sessions int; - v_i int; - v_hour int; - v_duration int; - v_started timestamptz; - v_ended timestamptz; - v_end_pct real; - v_delta real; - v_session_id text; - v_dow int; - v_file_ids int[]; - v_total_sessions int := 0; - v_total_completions int := 0; -BEGIN - -- Verify user exists - IF NOT EXISTS (SELECT 1 FROM users WHERE id = v_user_id) THEN - RAISE EXCEPTION 'User with id % not found', v_user_id; - END IF; - - -- Verify there are book files to work with - IF NOT EXISTS (SELECT 1 FROM book_files LIMIT 1) THEN - RAISE EXCEPTION 'No book files found - scan a library first'; - END IF; - - -- Pick up to 40 real file ids to spread sessions across - SELECT array_agg(id ORDER BY random()) INTO v_file_ids - FROM (SELECT id FROM book_files ORDER BY random() LIMIT 40) t; - - -- Spread books.added_at across the past 3 years so Format Share Over Time - -- chart has data across multiple months instead of a single spike today. - UPDATE books SET added_at = now() - (random() * interval '3 years') - WHERE added_at > now() - interval '7 days'; - - -- Insert sessions across the past 365 days - FOR v_day IN - SELECT generate_series( - current_date - interval '364 days', - current_date, - interval '1 day' - )::date - LOOP - v_dow := extract(dow from v_day); -- 0=Sun 6=Sat - - -- Weekends: 1-3 sessions. Weekdays: 0-2 (30% chance of 0). - IF v_dow IN (0, 6) THEN - v_sessions := 1 + floor(random() * 3)::int; - ELSIF random() < 0.30 THEN - v_sessions := 0; - ELSE - v_sessions := 1 + floor(random() * 2)::int; - END IF; - - FOR v_i IN 1..v_sessions LOOP - v_file_id := v_file_ids[1 + floor(random() * array_length(v_file_ids, 1))::int]; - - SELECT b.library_id INTO v_library_id - FROM book_files bf JOIN books b ON b.id = bf.book_id - WHERE bf.id = v_file_id; - - -- Realistic hour distribution: mostly evenings, some mornings/afternoons - IF random() < 0.60 THEN - v_hour := 18 + floor(random() * 6)::int; -- 18-23 evening - ELSIF random() < 0.50 THEN - v_hour := 7 + floor(random() * 3)::int; -- 7-9 morning - ELSE - v_hour := 12 + floor(random() * 4)::int; -- 12-15 afternoon - END IF; - - v_started := (v_day + make_interval(hours => v_hour, mins => floor(random() * 60)::int))::timestamptz; - v_duration := 600 + floor(random() * 3000)::int; -- 10-60 min - v_ended := v_started + make_interval(secs => v_duration); - v_delta := round((1 + random() * 8)::numeric, 4); -- 1-9% progress - -- ~15% of sessions are completions (every 7th deterministically by loop) - IF (v_total_sessions % 7) = 0 THEN - v_end_pct := 100; - v_total_completions := v_total_completions + 1; - ELSE - v_end_pct := round((10 + random() * 80)::numeric, 2); - END IF; - - v_session_id := md5(v_user_id::text || v_file_id::text || v_started::text || v_i::text); - - INSERT INTO reading_sessions - (user_id, book_file_id, session_id, started_at, ended_at, duration_seconds, progress_delta, end_progress) - VALUES - (v_user_id, v_file_id, v_session_id, v_started, v_ended, v_duration, v_delta, v_end_pct) - ON CONFLICT (session_id) DO NOTHING; - - -- Real-time upsert of daily stats (mirrors what the server does on session save) - INSERT INTO user_reading_daily_stats - (user_id, library_id, day, reading_seconds, progress_delta, sessions_count, updated_at) - VALUES - (v_user_id, v_library_id, v_day, v_duration, v_delta, 1, now()) - ON CONFLICT (user_id, library_id, day) DO UPDATE SET - reading_seconds = user_reading_daily_stats.reading_seconds + excluded.reading_seconds, - progress_delta = user_reading_daily_stats.progress_delta + excluded.progress_delta, - sessions_count = user_reading_daily_stats.sessions_count + 1, - updated_at = now(); - - v_total_sessions := v_total_sessions + 1; - END LOOP; - END LOOP; - - RAISE NOTICE 'Done: % sessions inserted (% completions)', v_total_sessions, v_total_completions; -END; -\$\$; - -EOSQL - -echo "Reading session seed complete." diff --git a/scripts/setup.sh b/scripts/setup.sh index 24629b7ad..ecddae8fd 100644 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -13,12 +13,10 @@ echo "Installing dependencies..." pnpm install echo "Starting PostgreSQL..." -docker compose up -d --wait +docker compose -f docker-compose.dev.yml up -d --wait echo "Applying migrations..." pnpm run db:migrate echo "Seeding baseline data..." pnpm run db:seed - -pnpm run doctor diff --git a/server/.env.example b/server/.env.example index c9c1a212f..7d8b32fb7 100644 --- a/server/.env.example +++ b/server/.env.example @@ -13,15 +13,14 @@ JWT_REFRESH_EXPIRES_IN=7d SETUP_BOOTSTRAP_TOKEN= # Storage -# App data directory for cover images and generated files. +# App data directory for covers, author images, user avatars, and cache. # This path resolves relative to server/ (where the process runs). # ../local/data points to /local/data. -# In production/Docker this is typically an absolute path like /data. BOOKS_PATH=../local/data -# Staging (optional — defaults to {BOOKS_PATH}/staging) -# Directory where staging files are uploaded and watched for new drops. -# STAGING_PATH=../local/data/staging +# Staging directory where files are dropped for auto-import. +# Defaults to {BOOKS_PATH}/staging if unset. +# STAGING_PATH=../local/staging # OIDC (optional — configure via admin settings UI; these are just for reference) # OIDC config is stored in the database (app_settings table, key: oidc_config) diff --git a/server/Dockerfile.dev b/server/Dockerfile.dev deleted file mode 100644 index eaf9cc5b2..000000000 --- a/server/Dockerfile.dev +++ /dev/null @@ -1,6 +0,0 @@ -FROM node:24-alpine -WORKDIR /app -COPY package.json package-lock.json ./ -RUN npm install -COPY . . -CMD ["npm", "run", "start:dev"] diff --git a/server/entrypoint.sh b/server/entrypoint.sh index 6d7603308..b586ddcfd 100644 --- a/server/entrypoint.sh +++ b/server/entrypoint.sh @@ -1,3 +1,12 @@ #!/bin/sh set -e + +if [ -z "$DATABASE_URL" ]; then + export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/${POSTGRES_DB}" +fi + +export BOOKS_PATH="${BOOKS_PATH:-/data}" +export STAGING_PATH="${STAGING_PATH:-/staging}" + +node dist/scripts/migrate.js exec node dist/main.js diff --git a/server/package.json b/server/package.json index e6804979d..ac971d928 100644 --- a/server/package.json +++ b/server/package.json @@ -60,13 +60,12 @@ "fast-xml-parser": "^5.5.9", "fastest-levenshtein": "^1.0.16", "fastify": "^5.8.4", - "geoip-lite": "^2.0.1", "jose": "^6.2.2", "ms": "^2.1.3", "music-metadata": "^11.12.3", "nestjs-pino": "^4.6.1", "node-unrar-js": "^2.0.2", - "nodemailer": "^8.0.3", + "nodemailer": "^8.0.4", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "pdf-lib": "^1.17.1", @@ -89,7 +88,6 @@ "@swc/core": "^1.15.21", "@types/bcrypt": "^6.0.0", "@types/express": "^5.0.6", - "@types/geoip-lite": "^1.4.4", "@types/node": "^25.5.0", "@types/nodemailer": "^6.4.23", "@types/passport-jwt": "^4.0.1", diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 87a021faf..7f309fe80 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -4,7 +4,7 @@ import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; import { ScheduleModule } from '@nestjs/schedule'; import { LoggerModule } from 'nestjs-pino'; -import { appConfig, authConfig, dbConfig, emailConfig, externalApiConfig, storageConfig } from './config/config'; +import { appConfig, authConfig, dbConfig, emailConfig, storageConfig } from './config/config'; import { validateEnv } from './config/env.validation'; import { loggerConfig } from './common/logger.config'; import { CommonModule } from './common/common.module'; @@ -58,7 +58,7 @@ import { AuditModule } from './modules/audit/audit.module'; ConfigModule.forRoot({ isGlobal: true, validate: validateEnv, - load: [appConfig, dbConfig, authConfig, storageConfig, externalApiConfig, emailConfig], + load: [appConfig, dbConfig, authConfig, storageConfig, emailConfig], }), ScheduleModule.forRoot(), DbModule, diff --git a/server/src/config/config.ts b/server/src/config/config.ts index 37410251f..6198707c4 100644 --- a/server/src/config/config.ts +++ b/server/src/config/config.ts @@ -1,9 +1,7 @@ import { registerAs } from '@nestjs/config'; export const appConfig = registerAs('app', () => ({ - port: parseInt(process.env.PORT ?? '3000', 10), nodeEnv: process.env.NODE_ENV ?? 'development', - logLevel: process.env.LOG_LEVEL, appUrl: process.env.APP_URL ?? 'http://localhost:5173', })); @@ -19,14 +17,10 @@ export const authConfig = registerAs('auth', () => ({ })); export const storageConfig = registerAs('storage', () => ({ - booksPath: process.env.BOOKS_PATH ?? '/data/books', + booksPath: process.env.BOOKS_PATH ?? '/data', stagingPath: process.env.STAGING_PATH, })); -export const externalApiConfig = registerAs('externalApi', () => ({ - googleBooksApiKey: process.env.GOOGLE_BOOKS_API_KEY ?? '', -})); - export const emailConfig = registerAs('email', () => ({ encryptionKey: process.env.EMAIL_ENCRYPTION_KEY ?? '', })); diff --git a/server/src/config/env.validation.ts b/server/src/config/env.validation.ts index 51428ab10..5d44d251a 100644 --- a/server/src/config/env.validation.ts +++ b/server/src/config/env.validation.ts @@ -2,8 +2,7 @@ import { z } from 'zod'; const envSchema = z.object({ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), - PORT: z.coerce.number().int().positive().default(3000), - DATABASE_URL: z.string().url('DATABASE_URL must be a valid URL'), + DATABASE_URL: z.string().url('DATABASE_URL must be a valid URL').optional(), JWT_SECRET: z .string() .min(16, 'JWT_SECRET must be at least 16 characters') @@ -14,10 +13,9 @@ const envSchema = z.object({ JWT_EXPIRES_IN: z.string().default('15m'), JWT_REFRESH_EXPIRES_IN: z.string().default('7d'), SETUP_BOOTSTRAP_TOKEN: z.string().optional(), - BOOKS_PATH: z.string().default('/data/books'), + BOOKS_PATH: z.string().default('/data'), STAGING_PATH: z.string().optional(), CLIENT_URL: z.string().url().optional(), - GOOGLE_BOOKS_API_KEY: z.string().default(''), APP_URL: z.string().url().default('http://localhost:5173'), EMAIL_ENCRYPTION_KEY: z.string().optional(), LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).optional(), diff --git a/server/src/db/migrations/0000_baseline.sql b/server/src/db/migrations/0000_baseline.sql index fb62dbc5b..75ebaecce 100644 --- a/server/src/db/migrations/0000_baseline.sql +++ b/server/src/db/migrations/0000_baseline.sql @@ -10,7 +10,6 @@ CREATE TABLE "audit_log" ( "resource_id" integer, "description" text NOT NULL, "ip" varchar(45), - "country_code" char(2), "meta" jsonb, "created_at" timestamp DEFAULT now() NOT NULL ); diff --git a/server/src/db/migrations/meta/0000_snapshot.json b/server/src/db/migrations/meta/0000_snapshot.json index 2cc4d6da0..ca8176385 100644 --- a/server/src/db/migrations/meta/0000_snapshot.json +++ b/server/src/db/migrations/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "044c15cb-ab99-4b1f-b88f-3c025644dec7", + "id": "2e38901e-f8a5-4f93-8564-e0f8f44e1883", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -56,12 +56,6 @@ "primaryKey": false, "notNull": false }, - "country_code": { - "name": "country_code", - "type": "char(2)", - "primaryKey": false, - "notNull": false - }, "meta": { "name": "meta", "type": "jsonb", diff --git a/server/src/db/migrations/meta/_journal.json b/server/src/db/migrations/meta/_journal.json index 3e45a2ef3..f0ae9a1a2 100644 --- a/server/src/db/migrations/meta/_journal.json +++ b/server/src/db/migrations/meta/_journal.json @@ -5,7 +5,7 @@ { "idx": 0, "version": "7", - "when": 1774336038252, + "when": 1774487725854, "tag": "0000_baseline", "breakpoints": true } diff --git a/server/src/db/schema/audit.ts b/server/src/db/schema/audit.ts index c6778ad41..c4a89ae17 100644 --- a/server/src/db/schema/audit.ts +++ b/server/src/db/schema/audit.ts @@ -1,4 +1,4 @@ -import { bigserial, char, index, integer, jsonb, pgTable, text, timestamp, varchar } from 'drizzle-orm/pg-core'; +import { bigserial, index, integer, jsonb, pgTable, text, timestamp, varchar } from 'drizzle-orm/pg-core'; import { users } from './auth'; @@ -13,7 +13,6 @@ export const auditLog = pgTable( resourceId: integer('resource_id'), description: text('description').notNull(), ip: varchar('ip', { length: 45 }), - countryCode: char('country_code', { length: 2 }), meta: jsonb('meta'), createdAt: timestamp('created_at').defaultNow().notNull(), }, diff --git a/server/src/modules/audit/audit.service.ts b/server/src/modules/audit/audit.service.ts index 0270b7940..869a5da50 100644 --- a/server/src/modules/audit/audit.service.ts +++ b/server/src/modules/audit/audit.service.ts @@ -1,5 +1,4 @@ import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; -import * as geoip from 'geoip-lite'; import { AUDIT_EVENT, AuditEventPayload, AuditEventsService } from './audit-events.service'; import { AuditRepository, AuditLogQuery } from './audit.repository'; @@ -30,8 +29,6 @@ export class AuditService implements OnModuleInit, OnModuleDestroy { } private handleAuditEvent(payload: AuditEventPayload): void { - const countryCode = payload.ip ? (geoip.lookup(payload.ip)?.country ?? null) : null; - this.auditRepository .insert({ userId: payload.userId, @@ -41,7 +38,6 @@ export class AuditService implements OnModuleInit, OnModuleDestroy { resourceId: payload.resourceId ?? null, description: payload.description, ip: payload.ip ?? null, - countryCode, meta: payload.meta ?? null, }) .catch((err: unknown) => { diff --git a/server/src/scripts/migrate.ts b/server/src/scripts/migrate.ts index e7891b39a..7ed175ce4 100644 --- a/server/src/scripts/migrate.ts +++ b/server/src/scripts/migrate.ts @@ -33,6 +33,12 @@ async function runMigrations() { }); try { + await pool.query(` + CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + CREATE EXTENSION IF NOT EXISTS "pg_trgm"; + CREATE EXTENSION IF NOT EXISTS "vector"; + `); + const migrationsFolder = resolveMigrationsFolder(); await migrate(drizzle(pool), { migrationsFolder });