From 16eebc93d8f83b307dec553692910dd123595f8f Mon Sep 17 00:00:00 2001 From: Icereed Date: Mon, 3 Feb 2025 09:04:12 +0100 Subject: [PATCH] Implement pagination for modification history and E2E tests (#162) --- .dockerignore | 6 +- .github/workflows/docker-build-and-push.yml | 67 +- .gitignore | 5 +- app_http_handlers.go | 25 +- local_db.go | 26 +- web-app/.env.test | 8 + web-app/docker-compose.test.yml | 57 + web-app/e2e/document-processing.spec.ts | 135 ++ web-app/e2e/fixtures/test-document.txt | 20 + web-app/e2e/setup/global-setup.ts | 24 + web-app/e2e/test-environment.ts | 281 ++++ web-app/package-lock.json | 1229 +++++++++++++++++- web-app/package.json | 14 +- web-app/playwright.config.ts | 36 + web-app/src/History.tsx | 89 +- web-app/src/components/DocumentCard.tsx | 2 +- web-app/src/components/SuccessModal.tsx | 2 +- web-app/src/components/SuggestionsReview.tsx | 2 +- web-app/src/components/UndoCard.tsx | 4 +- 19 files changed, 1981 insertions(+), 51 deletions(-) create mode 100644 web-app/.env.test create mode 100644 web-app/docker-compose.test.yml create mode 100644 web-app/e2e/document-processing.spec.ts create mode 100644 web-app/e2e/fixtures/test-document.txt create mode 100644 web-app/e2e/setup/global-setup.ts create mode 100644 web-app/e2e/test-environment.ts create mode 100644 web-app/playwright.config.ts diff --git a/.dockerignore b/.dockerignore index 8a4c8c7..76dfb57 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,7 @@ .env Dockerfile -.github \ No newline at end of file +web-app/e2e +web-app/node_modules +web-app/playwright_report +web-app/test-results +.github diff --git a/.github/workflows/docker-build-and-push.yml b/.github/workflows/docker-build-and-push.yml index f252f71..e68c4bb 100644 --- a/.github/workflows/docker-build-and-push.yml +++ b/.github/workflows/docker-build-and-push.yml @@ -1,5 +1,9 @@ name: Build and Push Docker Images +permissions: + pull-requests: write + contents: read + on: push: branches: @@ -16,29 +20,22 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - - name: Set up Go uses: actions/setup-go@v5 with: go-version: 1.22 - - name: Install mupdf run: sudo apt-get install -y mupdf - - name: Set library path run: echo "/usr/lib" | sudo tee -a /etc/ld.so.conf.d/mupdf.conf && sudo ldconfig - - name: Install dependencies run: go mod download - - name: Run Go tests run: go test ./... - - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 20 - - name: Cache npm dependencies uses: actions/cache@v4 with: @@ -46,11 +43,9 @@ jobs: key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - - name: Install frontend dependencies run: npm install working-directory: web-app - - name: Run frontend tests run: npm test working-directory: web-app @@ -60,13 +55,13 @@ jobs: needs: test outputs: digest: ${{ steps.build_amd64.outputs.digest }} + image_tag: ${{ steps.set_image_tag.outputs.image_tag }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to Docker Hub - if: ${{ github.event_name != 'pull_request' }} uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} @@ -74,7 +69,9 @@ jobs: - name: Set Docker tags id: set_tags run: | - if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then + if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then + echo "TAGS=icereed/paperless-gpt:pr-${GITHUB_SHA}-amd64" >> $GITHUB_ENV + elif [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then VERSION=${GITHUB_REF#refs/tags/} echo "TAGS=icereed/paperless-gpt:${VERSION}-amd64" >> $GITHUB_ENV else @@ -86,7 +83,7 @@ jobs: with: context: . platforms: linux/amd64 - push: ${{ github.event_name != 'pull_request' }} + push: true cache-from: type=gha cache-to: type=gha,mode=max tags: ${{ env.TAGS }} @@ -94,6 +91,9 @@ jobs: VERSION=${{ github.ref_type == 'tag' && github.ref_name || github.sha }} COMMIT=${{ github.sha }} BUILD_DATE=${{ github.event.repository.pushed_at }} + - name: Set image tag output + id: set_image_tag + run: echo "image_tag=${TAGS}" >> $GITHUB_OUTPUT - name: Export digest for amd64 run: | mkdir -p ${{ runner.temp }}/digests @@ -203,4 +203,45 @@ jobs: docker buildx imagetools inspect ${DOCKERHUB_REPO}:${VERSION} if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then docker buildx imagetools inspect ${DOCKERHUB_REPO}:latest - fi \ No newline at end of file + fi + + e2e-tests: + name: E2E Tests + needs: build-amd64 + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./web-app + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: './web-app/package-lock.json' + - name: Install dependencies + run: npm ci + - name: Install Playwright browsers + run: npx playwright install chromium --with-deps + - name: Run Playwright tests + run: npm run test:e2e + env: + CI: true + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + PAPERLESS_GPT_IMAGE: icereed/paperless-gpt:pr-${{ github.sha }}-amd64 + - name: Upload Playwright Report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: web-app/playwright-report/ + retention-days: 30 + - name: Upload test screenshots + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: web-app/test-results/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index e2c91ba..cf774a4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,7 @@ prompts/ tests/tmp tmp/ -db/ \ No newline at end of file +db/ +web-app/playwright-report/ +web-app/test-results/.last-run.json +web-app/test-results diff --git a/app_http_handlers.go b/app_http_handlers.go index 58809ec..85a944e 100644 --- a/app_http_handlers.go +++ b/app_http_handlers.go @@ -248,13 +248,34 @@ func (app *App) getDocumentHandler() gin.HandlerFunc { // Section for local-db actions func (app *App) getModificationHistoryHandler(c *gin.Context) { - modifications, err := GetAllModifications(app.Database) + // Parse pagination parameters + page := 1 + pageSize := 20 + + if p, err := strconv.Atoi(c.DefaultQuery("page", "1")); err == nil && p > 0 { + page = p + } + if ps, err := strconv.Atoi(c.DefaultQuery("pageSize", "20")); err == nil && ps > 0 && ps <= 100 { + pageSize = ps + } + + // Get paginated modifications and total count + modifications, total, err := GetPaginatedModifications(app.Database, page, pageSize) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve modification history"}) log.Errorf("Failed to retrieve modification history: %v", err) return } - c.JSON(http.StatusOK, modifications) + + totalPages := (int(total) + pageSize - 1) / pageSize + + c.JSON(http.StatusOK, gin.H{ + "items": modifications, + "totalItems": total, + "totalPages": totalPages, + "currentPage": page, + "pageSize": pageSize, + }) } func (app *App) undoModificationHandler(c *gin.Context) { diff --git a/local_db.go b/local_db.go index 931d7fb..2d696f2 100644 --- a/local_db.go +++ b/local_db.go @@ -63,13 +63,35 @@ func GetModification(db *gorm.DB, id uint) (*ModificationHistory, error) { return &record, result.Error } -// GetAllModifications retrieves all modification records from the database +// GetAllModifications retrieves all modification records from the database (deprecated - use GetPaginatedModifications instead) func GetAllModifications(db *gorm.DB) ([]ModificationHistory, error) { var records []ModificationHistory - result := db.Order("date_changed DESC").Find(&records) // GORM's Find method retrieves all records + result := db.Order("date_changed DESC").Find(&records) return records, result.Error } +// GetPaginatedModifications retrieves a page of modification records with total count +func GetPaginatedModifications(db *gorm.DB, page int, pageSize int) ([]ModificationHistory, int64, error) { + var records []ModificationHistory + var total int64 + + // Get total count + if err := db.Model(&ModificationHistory{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + // Calculate offset + offset := (page - 1) * pageSize + + // Get paginated records + result := db.Order("date_changed DESC"). + Offset(offset). + Limit(pageSize). + Find(&records) + + return records, total, result.Error +} + // UndoModification marks a modification record as undone and sets the undo date func SetModificationUndone(db *gorm.DB, record *ModificationHistory) error { record.Undone = true diff --git a/web-app/.env.test b/web-app/.env.test new file mode 100644 index 0000000..85a6531 --- /dev/null +++ b/web-app/.env.test @@ -0,0 +1,8 @@ +# Environment variables for E2E tests +PAPERLESS_NGX_URL=http://localhost:8000 +PAPERLESS_GPT_URL=http://localhost:8080 +PAPERLESS_ADMIN_USER=admin +PAPERLESS_ADMIN_PASSWORD=admin + +# Add your OpenAI API key here (required for document processing) +# OPENAI_API_KEY=sk-uhB.... # Replace with your actual OpenAI API key from https://platform.openai.com/api-keys diff --git a/web-app/docker-compose.test.yml b/web-app/docker-compose.test.yml new file mode 100644 index 0000000..c409419 --- /dev/null +++ b/web-app/docker-compose.test.yml @@ -0,0 +1,57 @@ +version: '3.8' + +services: + paperless-ngx: + image: ghcr.io/paperless-ngx/paperless-ngx:latest + environment: + PAPERLESS_URL: http://localhost:8001 + PAPERLESS_SECRET_KEY: change-me + PAPERLESS_ADMIN_USER: admin + PAPERLESS_ADMIN_PASSWORD: admin + PAPERLESS_TIME_ZONE: Europe/Berlin + PAPERLESS_OCR_LANGUAGE: eng + PAPERLESS_REDIS: redis://redis:6379 + ports: + - "8001:8000" + volumes: + - paperless-data:/usr/src/paperless/data + - paperless-media:/usr/src/paperless/media + - paperless-export:/usr/src/paperless/export + depends_on: + - redis + - postgres + + redis: + image: redis:7 + restart: unless-stopped + + postgres: + image: postgres:15 + restart: unless-stopped + environment: + POSTGRES_DB: paperless + POSTGRES_USER: paperless + POSTGRES_PASSWORD: paperless + volumes: + - pgdata:/var/lib/postgresql/data + + paperless-gpt: + build: + context: .. + dockerfile: Dockerfile + image: icereed/paperless-gpt:e2e + environment: + PAPERLESS_URL: http://paperless-ngx:8000 + PAPERLESS_ADMIN_USER: admin + PAPERLESS_ADMIN_PASSWORD: admin + OPENAI_API_KEY: ${OPENAI_API_KEY} + ports: + - "8080:8080" + depends_on: + - paperless-ngx + +volumes: + paperless-data: + paperless-media: + paperless-export: + pgdata: diff --git a/web-app/e2e/document-processing.spec.ts b/web-app/e2e/document-processing.spec.ts new file mode 100644 index 0000000..3a17cf5 --- /dev/null +++ b/web-app/e2e/document-processing.spec.ts @@ -0,0 +1,135 @@ +import { expect, Page, test } from '@playwright/test'; +import path, { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { addTagToDocument, PORTS, setupTestEnvironment, TestEnvironment, uploadDocument } from './test-environment'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +let testEnv: TestEnvironment; +let page: Page; + +test.beforeAll(async () => { + testEnv = await setupTestEnvironment(); +}); + +test.afterAll(async () => { + await testEnv.cleanup(); +}); + +test.beforeEach(async ({ page: testPage }) => { + page = testPage; + await page.goto(`http://localhost:${testEnv.paperlessGpt.getMappedPort(PORTS.paperlessGpt)}`); + await page.screenshot({ path: 'test-results/initial-state.png' }); +}); + +test.afterEach(async () => { + await page.close(); +}); + +test('should process document and show changes in history', async () => { + const paperlessNgxPort = testEnv.paperlessNgx.getMappedPort(PORTS.paperlessNgx); + const paperlessGptPort = testEnv.paperlessGpt.getMappedPort(PORTS.paperlessGpt); + const credentials = { username: 'admin', password: 'admin' }; + + // 1. Upload document and add initial tag via API + const baseUrl = `http://localhost:${paperlessNgxPort}`; + const documentPath = path.join(__dirname, 'fixtures', 'test-document.txt'); + + // Get the paperless-gpt tag ID + const response = await fetch(`${baseUrl}/api/tags/?name=paperless-gpt`, { + headers: { + 'Authorization': 'Basic ' + btoa(`${credentials.username}:${credentials.password}`), + }, + }); + + if (!response.ok) { + throw new Error('Failed to fetch paperless-gpt tag'); + } + + const tags = await response.json(); + if (!tags.results || tags.results.length === 0) { + throw new Error('paperless-gpt tag not found'); + } + + const tagId = tags.results[0].id; + + // Upload document and get ID + const { id: documentId } = await uploadDocument( + baseUrl, + documentPath, + 'Original Title', + credentials + ); + + console.log(`Document ID: ${documentId}`); + + // Add tag to document + await addTagToDocument( + baseUrl, + documentId, + tagId, + credentials + ); + + // 2. Navigate to Paperless-GPT UI and process the document + await page.goto(`http://localhost:${paperlessGptPort}`); + + // Wait for document to appear in the list + await page.waitForSelector('.document-card', { timeout: 1000 * 60 }); + await page.screenshot({ path: 'test-results/document-loaded.png' }); + + // Click the process button + await page.click('button:has-text("Generate Suggestions")'); + + // Wait for processing to complete + await page.waitForSelector('.suggestions-review', { timeout: 30000 }); + await page.screenshot({ path: 'test-results/suggestions-loaded.png' }); + + // Apply the suggestions + await page.click('button:has-text("Apply")'); + + // Wait for success message + await page.waitForSelector('.success-modal', { timeout: 10000 }); + await page.screenshot({ path: 'test-results/suggestions-applied.png' }); + + // Click "OK" on success modal + await page.click('button:has-text("OK")'); + + // 3. Check history page for the modifications + await page.click('a:has-text("History")'); + + // Wait for history page to load + await page.waitForSelector('.modification-history', { timeout: 5000 }); + await page.screenshot({ path: 'test-results/history-page.png' }); + + // Verify at least one modification entry exists + const modifications = await page.locator('.undo-card').count(); + expect(modifications).toBeGreaterThan(0); + + // Verify modification details + const firstModification = await page.locator('.undo-card').first(); + + // Check if title was modified + const titleChange = await firstModification.locator('text=Original Title').isVisible(); + expect(titleChange).toBeTruthy(); + + // Test pagination if there are multiple modifications + const paginationVisible = await page.locator('.pagination-controls').isVisible(); + if (paginationVisible) { + // Click next page if available + const nextButton = page.locator('button:has-text("Next")'); + if (await nextButton.isEnabled()) { + await nextButton.click(); + // Wait for new items to load + await page.waitForSelector('.undo-card'); + } + } + + // 4. Test undo functionality + const undoButton = await firstModification.locator('button:has-text("Undo")'); + if (await undoButton.isEnabled()) { + await undoButton.click(); + // Wait for undo to complete. Text should change to "Undone" + await page.waitForSelector('text=Undone'); + } +}); diff --git a/web-app/e2e/fixtures/test-document.txt b/web-app/e2e/fixtures/test-document.txt new file mode 100644 index 0000000..20cbda3 --- /dev/null +++ b/web-app/e2e/fixtures/test-document.txt @@ -0,0 +1,20 @@ +Invoice #12345 + +From: Test Company Ltd. +To: Sample Client Inc. + +Date: January 27, 2025 + +Item Description: +1. Software Development Services - $5,000 +2. Cloud Infrastructure Setup - $2,500 +3. Technical Consulting - $1,500 + +Total: $9,000 + +Payment Terms: Net 30 +Due Date: February 26, 2025 + +Please make payment to: +Bank: Test Bank +Account: 1234567890 diff --git a/web-app/e2e/setup/global-setup.ts b/web-app/e2e/setup/global-setup.ts new file mode 100644 index 0000000..44677fa --- /dev/null +++ b/web-app/e2e/setup/global-setup.ts @@ -0,0 +1,24 @@ +import { chromium } from '@playwright/test'; +import * as nodeFetch from 'node-fetch'; + +// Polyfill fetch for Node.js environment +if (!globalThis.fetch) { + (globalThis as any).fetch = nodeFetch.default; + (globalThis as any).Headers = nodeFetch.Headers; + (globalThis as any).Request = nodeFetch.Request; + (globalThis as any).Response = nodeFetch.Response; + (globalThis as any).FormData = nodeFetch.FormData; +} + +async function globalSetup() { + // Install Playwright browser if needed + const browser = await chromium.launch(); + await browser.close(); + + // Load environment variables + if (!process.env.OPENAI_API_KEY) { + console.warn('Warning: OPENAI_API_KEY environment variable is not set'); + } +} + +export default globalSetup; diff --git a/web-app/e2e/test-environment.ts b/web-app/e2e/test-environment.ts new file mode 100644 index 0000000..22bb58e --- /dev/null +++ b/web-app/e2e/test-environment.ts @@ -0,0 +1,281 @@ +import { Browser, chromium } from '@playwright/test'; +import * as fs from 'fs'; +import { GenericContainer, Network, StartedTestContainer, Wait } from 'testcontainers'; + +export interface TestEnvironment { + paperlessNgx: StartedTestContainer; + paperlessGpt: StartedTestContainer; + browser: Browser; + cleanup: () => Promise; +} + +export const PORTS = { + paperlessNgx: 8000, + paperlessGpt: 8080, +}; + +export async function setupTestEnvironment(): Promise { + console.log('Setting up test environment...'); + const paperlessPort = PORTS.paperlessNgx; + const gptPort = PORTS.paperlessGpt; + + // Create a network for the containers + const network = await new Network().start(); + + console.log('Starting Redis container...'); + const redis = await new GenericContainer('redis:7') + .withNetwork(network) + .withNetworkAliases('redis') + .start(); + + console.log('Starting Postgres container...'); + const postgres = await new GenericContainer('postgres:15') + .withNetwork(network) + .withNetworkAliases('postgres') + .withEnvironment({ + POSTGRES_DB: 'paperless', + POSTGRES_USER: 'paperless', + POSTGRES_PASSWORD: 'paperless' + }) + .start(); + + console.log('Starting Paperless-ngx container...'); + const paperlessNgx = await new GenericContainer('ghcr.io/paperless-ngx/paperless-ngx:latest') + .withNetwork(network) + .withNetworkAliases('paperless-ngx') + .withEnvironment({ + PAPERLESS_URL: `http://localhost:${paperlessPort}`, + PAPERLESS_SECRET_KEY: 'change-me', + PAPERLESS_ADMIN_USER: 'admin', + PAPERLESS_ADMIN_PASSWORD: 'admin', + PAPERLESS_TIME_ZONE: 'Europe/Berlin', + PAPERLESS_OCR_LANGUAGE: 'eng', + PAPERLESS_REDIS: 'redis://redis:6379', + PAPERLESS_DBHOST: 'postgres', + PAPERLESS_DBNAME: 'paperless', + PAPERLESS_DBUSER: 'paperless', + PAPERLESS_DBPASS: 'paperless' + }) + .withExposedPorts(paperlessPort) + .withWaitStrategy(Wait.forHttp('/api/', paperlessPort)) + .start(); + + const mappedPort = paperlessNgx.getMappedPort(paperlessPort); + console.log(`Paperless-ngx container started, mapped port: ${mappedPort}`); + // Create required tag before starting paperless-gpt + const baseUrl = `http://localhost:${mappedPort}`; + const credentials = { username: 'admin', password: 'admin' }; + + try { + console.log('Creating paperless-gpt tag...'); + await createTag(baseUrl, 'paperless-gpt', credentials); + } catch (error) { + console.error('Failed to create paperless-gpt tag:', error); + await paperlessNgx.stop(); + throw error; + } + + console.log('Starting Paperless-gpt container...'); + const paperlessGptImage = process.env.PAPERLESS_GPT_IMAGE || 'icereed/paperless-gpt:e2e'; + console.log(`Using image: ${paperlessGptImage}`); + const paperlessGpt = await new GenericContainer(paperlessGptImage) + .withNetwork(network) + .withEnvironment({ + PAPERLESS_BASE_URL: `http://paperless-ngx:${paperlessPort}`, + PAPERLESS_API_TOKEN: await getApiToken(baseUrl, credentials), + LLM_PROVIDER: "openai", + LLM_MODEL: "gpt-4o-mini", + LLM_LANGUAGE: "english", + OPENAI_API_KEY: process.env.OPENAI_API_KEY || '', + }) + .withExposedPorts(gptPort) + .withWaitStrategy(Wait.forHttp('/', gptPort)) + .start(); + console.log('Paperless-gpt container started'); + + console.log('Launching browser...'); + const browser = await chromium.launch(); + console.log('Browser launched'); + + const cleanup = async () => { + console.log('Cleaning up test environment...'); + await browser.close(); + await paperlessGpt.stop(); + await paperlessNgx.stop(); + await redis.stop(); + await postgres.stop(); + await network.stop(); + console.log('Test environment cleanup completed'); + }; + + console.log('Test environment setup completed'); + return { + paperlessNgx, + paperlessGpt, + browser, + cleanup, + }; +} + +export async function waitForElement(page: any, selector: string, timeout = 5000): Promise { + await page.waitForSelector(selector, { timeout }); +} + +export interface PaperlessDocument { + id: number; + title: string; + content: string; + tags: number[]; +} + +// Helper to upload a document via Paperless-ngx API +export async function uploadDocument( + baseUrl: string, + filePath: string, + title: string, + credentials: { username: string; password: string } +): Promise { + console.log(`Uploading document: ${title} from ${filePath}`); + const formData = new FormData(); + const fileData = await fs.promises.readFile(filePath); + formData.append('document', new Blob([fileData])); + formData.append('title', title); + + // Initial upload to get task ID + const uploadResponse = await fetch(`${baseUrl}/api/documents/post_document/`, { + method: 'POST', + body: formData, + headers: { + 'Authorization': 'Basic ' + btoa(`${credentials.username}:${credentials.password}`), + }, + }); + + if (!uploadResponse.ok) { + console.error(`Upload failed with status ${uploadResponse.status}: ${uploadResponse.statusText}`); + throw new Error(`Failed to upload document: ${uploadResponse.statusText}`); + } + + const task_id = await uploadResponse.json(); + + // Poll the tasks endpoint until document is processed + while (true) { + console.log(`Checking task status for ID: ${task_id}`); + const taskResponse = await fetch(`${baseUrl}/api/tasks/?task_id=${task_id}`, { + headers: { + 'Authorization': 'Basic ' + btoa(`${credentials.username}:${credentials.password}`), + }, + }); + + if (!taskResponse.ok) { + throw new Error(`Failed to check task status: ${taskResponse.statusText}`); + } + + const taskResultArr = await taskResponse.json(); + console.log(`Task status: ${JSON.stringify(taskResultArr)}`); + + if (taskResultArr.length === 0) { + continue; + } + const taskResult = taskResultArr[0]; + // Check if task is completed + if (taskResult.status === 'SUCCESS' && taskResult.id) { + console.log(`Document processed successfully with ID: ${taskResult.id}`); + + // Fetch the complete document details + const documentResponse = await fetch(`${baseUrl}/api/documents/${taskResult.id}/`, { + headers: { + 'Authorization': 'Basic ' + btoa(`${credentials.username}:${credentials.password}`), + }, + }); + + if (!documentResponse.ok) { + throw new Error(`Failed to fetch document details: ${documentResponse.statusText}`); + } + + return await documentResponse.json(); + } + + // Check for failure + if (taskResult.status === 'FAILED') { + throw new Error(`Document processing failed: ${taskResult.result}`); + } + + // Wait before polling again + await new Promise(resolve => setTimeout(resolve, 1000)); + } +} +// Helper to create a tag via Paperless-ngx API +export async function createTag( + baseUrl: string, + name: string, + credentials: { username: string; password: string } +): Promise { + console.log(`Creating tag: ${name}`); + const response = await fetch(`${baseUrl}/api/tags/`, { + method: 'POST', + body: JSON.stringify({ name }), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Basic ' + btoa(`${credentials.username}:${credentials.password}`), + }, + }); + + if (!response.ok) { + console.error(`Tag creation failed with status ${response.status}: ${response.statusText}`); + throw new Error(`Failed to create tag: ${response.statusText}`); + } + + const tag = await response.json(); + console.log(`Tag created successfully with ID: ${tag.id}`); + return tag.id; +} + +// Helper to get an API token +export async function getApiToken( + baseUrl: string, + credentials: { username: string; password: string } +): Promise { + console.log('Fetching API token'); + const response = await fetch(`${baseUrl}/api/token/`, { + method: 'POST', + body: new URLSearchParams({ + username: credentials.username, + password: credentials.password, + }), + }); + + if (!response.ok) { + console.error(`API token fetch failed with status ${response.status}: ${response.statusText}`); + throw new Error(`Failed to fetch API token: ${response.statusText}`); + } + + const token = await response.json(); + console.log(`API token fetched successfully: ${token.token}`); + return token.token; +} + +// Helper to add a tag to a document +export async function addTagToDocument( + baseUrl: string, + documentId: number, + tagId: number, + credentials: { username: string; password: string } +): Promise { + console.log(`Adding tag ${tagId} to document ${documentId}`); + const response = await fetch(`${baseUrl}/api/documents/${documentId}/`, { + method: 'PATCH', + body: JSON.stringify({ + tags: [tagId], + }), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Basic ' + btoa(`${credentials.username}:${credentials.password}`), + }, + }); + + if (!response.ok) { + console.error(`Tag addition failed with status ${response.status}: ${response.statusText}`); + throw new Error(`Failed to add tag to document: ${response.statusText}`); + } + console.log('Tag added successfully'); +} diff --git a/web-app/package-lock.json b/web-app/package-lock.json index 4e4492a..d9442de 100644 --- a/web-app/package-lock.json +++ b/web-app/package-lock.json @@ -26,17 +26,21 @@ }, "devDependencies": { "@eslint/js": "^9.9.0", - "@types/node": "^22.10.1", + "@playwright/test": "^1.50.0", + "@types/node": "^22.10.10", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react-swc": "^3.5.0", "autoprefixer": "^10.4.20", + "dotenv": "^16.4.7", "eslint": "^9.9.0", "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.9", "globals": "^15.9.0", + "node-fetch": "^3.3.0", "postcss": "^8.4.47", "tailwindcss": "^3.4.12", + "testcontainers": "^10.17.1", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", "vite": "^5.4.1" @@ -54,6 +58,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@colors/colors": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", @@ -555,6 +566,16 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/@floating-ui/core": { "version": "1.6.8", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", @@ -806,6 +827,21 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0.tgz", + "integrity": "sha512-ZGNXbt+d65EGjBORQHuYKj+XhCewlwpnSd/EDuLPZGSiEWmgOJB5RmMCCYGy5aMfTs9wx61RivfDKi8H/hcMvw==", + "dev": true, + "dependencies": { + "playwright": "1.50.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@react-aria/focus": { "version": "3.18.2", "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.18.2.tgz", @@ -1346,6 +1382,29 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "3.3.34", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.34.tgz", + "integrity": "sha512-mH9SuIb8NuTDsMus5epcbTzSbEo52fKLBMo0zapzYIAIyfDqoIFn7L3trekHLKC8qmxGV++pPUP4YqQ9n5v2Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*", + "@types/ssh2": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -1353,11 +1412,10 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.10.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", - "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", + "version": "22.10.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz", + "integrity": "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==", "dev": true, - "license": "MIT", "dependencies": { "undici-types": "~6.20.0" } @@ -1387,6 +1445,43 @@ "@types/react": "*" } }, + "node_modules/@types/ssh2": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.4.tgz", + "integrity": "sha512-9JTQgVBWSgq6mAen6PVnrAmty1lqgCMvpfN+1Ck5WRUsyMYPa6qd50/vMJ0y1zkGpOEgLzm8m8Dx/Y5vRouLaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2-streams": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.12.tgz", + "integrity": "sha512-Sy8tpEmCce4Tq0oSOYdfqaBpA3hDM8SoxoFh5vzFsu2oL+znzGz8oVWW7xb4K920yYMUY+PIG31qZnFMfPWNCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.74", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.74.tgz", + "integrity": "sha512-HMwEkkifei3L605gFdV+/UwtpxP6JSzM+xFk2Ia6DNFSwSVBRh9qp5Tgf4lNFOMfPVuU0WnkcWpXZpgn5ufO4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", @@ -1623,6 +1718,19 @@ "vite": "^4 || ^5 || ^6" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -1703,6 +1811,78 @@ "node": ">= 8" } }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -1715,11 +1895,28 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1773,12 +1970,119 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bare-events": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-fs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.1.tgz", + "integrity": "sha512-ilQs4fm/l9eMfWY2dY0WCIUplSUp7U0CT1vrqMg1MUdeZl4fypu5UP0XcDBK5WBQPJAKP1b7XEodISmekH/CEg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^3.0.0", + "bare-stream": "^2.0.0" + }, + "engines": { + "bare": ">=1.7.0" + } + }, + "node_modules/bare-os": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.4.0.tgz", + "integrity": "sha512-9Ous7UlnKbe3fMi7Y+qh0DwAup6A1JkYgPnjvMDNOlmnxNRQvQ/7Nst+OnUQKzk0iAT0m9BisbDVp9gCv8+ETA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.6.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.4.tgz", + "integrity": "sha512-G6i3A74FjNq4nVrrSTUz5h3vgXzBJnjmWAVlBWaZETkgu+LgKd7AiyOml3EDJY1AHlIbBHKDXE+TUT53Ff8OaA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1791,6 +2095,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1845,6 +2186,61 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buildcheck": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1935,6 +2331,13 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -2025,12 +2428,112 @@ "node": ">= 6" } }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2063,6 +2566,15 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "dev": true }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/date-fns": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", @@ -2115,6 +2627,92 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, + "node_modules/docker-compose": { + "version": "0.24.8", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.8.tgz", + "integrity": "sha512-plizRs/Vf15H+GCVxq2EUvyPK7ei9b/cVesHvjnX4xaXjM9spHe2Ytq0BitndFgvTJ3E3NljPNUEl7BAN43iZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.2.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/docker-modem": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.8.tgz", + "integrity": "sha512-f0ReSURdM3pcKPNS30mxOHSbaFLcknGmQjwSfmbcdOw1XWKXVhukM3NJHhr7NpY9BIyyWQb0EBo3KQvvuU5egQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^1.11.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/dockerode": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.3.5.tgz", + "integrity": "sha512-/0YNa3ZDNeLr/tSckmD69+Gq+qVNhvKfAHNeZJBnp7EOP6RGKV8ORrJHkUn20So5wU+xxT7+1n5u8PjHbfjbSA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "docker-modem": "^3.0.0", + "tar-fs": "~2.0.1" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/dockerode/node_modules/tar-fs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", + "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + } + }, + "node_modules/dockerode/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2138,6 +2736,16 @@ "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -2365,12 +2973,39 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -2425,6 +3060,29 @@ "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2537,6 +3195,18 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -2550,6 +3220,13 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2573,6 +3250,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -2642,6 +3332,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2669,6 +3366,27 @@ "node": ">= 0.4" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2799,6 +3517,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2878,6 +3603,52 @@ "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -2925,6 +3696,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3026,6 +3804,26 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3042,6 +3840,14 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -3067,6 +3873,43 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz", + "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==", + "dev": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -3108,6 +3951,16 @@ "node": ">= 6" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/one-time": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", @@ -3258,6 +4111,50 @@ "node": ">= 6" } }, + "node_modules/playwright": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0.tgz", + "integrity": "sha512-+GinGfGTrd2IfX1TA4N2gNmeIksSb+IAe589ZH+FlmpV3MYTx6+buChGIuDLQwrGNCw2lWibqV50fU510N7S+w==", + "dev": true, + "dependencies": { + "playwright-core": "1.50.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0.tgz", + "integrity": "sha512-CXkSSlr4JaZs2tZHI40DsZUN/NIwgaUPsyLuOAaIZp2CyF2sN5MM5NJsyB188lFSSozFxQ5fPT4qM+f0tH/6wQ==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", @@ -3411,6 +4308,23 @@ "node": ">= 0.8.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -3421,11 +4335,58 @@ "react-is": "^16.13.1" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/properties-reader": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/properties-reader/-/properties-reader-2.3.0.tgz", + "integrity": "sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/properties?sponsor=1" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3455,6 +4416,13 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true, + "license": "MIT" + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -3571,6 +4539,39 @@ "node": ">= 6" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -3609,6 +4610,16 @@ "node": ">=4" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -3704,6 +4715,13 @@ "node": ">=10" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -3774,6 +4792,53 @@ "node": ">=0.10.0" } }, + "node_modules/split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ssh-remote-port-forward": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz", + "integrity": "sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ssh2": "^0.5.48", + "ssh2": "^1.4.0" + } + }, + "node_modules/ssh-remote-port-forward/node_modules/@types/ssh2": { + "version": "0.5.52", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz", + "integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2-streams": "*" + } + }, + "node_modules/ssh2": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", + "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.20.0" + } + }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -3782,6 +4847,21 @@ "node": "*" } }, + "node_modules/streamx": { + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.1.tgz", + "integrity": "sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -3981,6 +5061,67 @@ "node": ">=14.0.0" } }, + "node_modules/tar-fs": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz", + "integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/testcontainers": { + "version": "10.17.1", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.17.1.tgz", + "integrity": "sha512-pYwpm6iH1UtZFVoSWjfUol4JCMyX4UksA5fwDotlTp2GgMqoHud+A+PY60kYUBVdSJJ/5AsSqhhFRvoK4ijISg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@types/dockerode": "^3.3.29", + "archiver": "^7.0.1", + "async-lock": "^1.4.1", + "byline": "^5.0.0", + "debug": "^4.3.5", + "docker-compose": "^0.24.8", + "dockerode": "^3.3.5", + "get-port": "^5.1.1", + "proper-lockfile": "^4.1.2", + "properties-reader": "^2.3.0", + "ssh-remote-port-forward": "^1.0.4", + "tar-fs": "^3.0.6", + "tmp": "^0.2.3", + "undici": "^5.28.4" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -4013,6 +5154,16 @@ "node": ">=0.8" } }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4056,6 +5207,13 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4105,6 +5263,19 @@ } } }, + "node_modules/undici": { + "version": "5.28.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz", + "integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", @@ -4215,6 +5386,15 @@ } } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4367,6 +5547,13 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, "node_modules/yaml": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", @@ -4390,6 +5577,38 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } } } } diff --git a/web-app/package.json b/web-app/package.json index c8cd1ef..f8d3ce7 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -8,7 +8,11 @@ "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", - "test": "echo \"TODO\"" + "test": "echo \"TODO\"", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "docker:test:up": "docker compose -f docker-compose.test.yml up -d", + "docker:test:down": "docker compose -f docker-compose.test.yml down -v" }, "dependencies": { "@headlessui/react": "^2.1.8", @@ -29,19 +33,23 @@ }, "devDependencies": { "@eslint/js": "^9.9.0", - "@types/node": "^22.10.1", + "@playwright/test": "^1.50.0", + "@types/node": "^22.10.10", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react-swc": "^3.5.0", "autoprefixer": "^10.4.20", + "dotenv": "^16.4.7", "eslint": "^9.9.0", "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.9", "globals": "^15.9.0", + "node-fetch": "^3.3.0", "postcss": "^8.4.47", "tailwindcss": "^3.4.12", + "testcontainers": "^10.17.1", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", "vite": "^5.4.1" } -} +} \ No newline at end of file diff --git a/web-app/playwright.config.ts b/web-app/playwright.config.ts new file mode 100644 index 0000000..61586a6 --- /dev/null +++ b/web-app/playwright.config.ts @@ -0,0 +1,36 @@ +import { defineConfig, devices } from '@playwright/test'; +import dotenv from 'dotenv'; + +// Load test environment variables +dotenv.config({ path: '.env.test' }); + +export default defineConfig({ + testDir: './e2e', + timeout: 120000, // Increased timeout for container startup + expect: { + timeout: 10000, + }, + globalSetup: './e2e/setup/global-setup.ts', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, + reporter: 'html', + use: { + screenshot: 'on', + baseURL: process.env.PAPERLESS_GPT_URL || 'http://localhost:8080', + trace: 'retain-on-failure', + video: 'retain-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + viewport: { width: 1280, height: 720 }, + }, + }, + ], + outputDir: 'test-results', + preserveOutput: 'failures-only', +}); diff --git a/web-app/src/History.tsx b/web-app/src/History.tsx index e68fb57..ed57a7b 100644 --- a/web-app/src/History.tsx +++ b/web-app/src/History.tsx @@ -12,11 +12,23 @@ interface ModificationHistory { UndoneDate: string | null; } +interface PaginatedResponse { + items: ModificationHistory[]; + totalItems: number; + totalPages: number; + currentPage: number; + pageSize: number; +} + const History: React.FC = () => { const [modifications, setModifications] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [paperlessUrl, setPaperlessUrl] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [totalItems, setTotalItems] = useState(0); + const pageSize = 20; // Get Paperless URL useEffect(() => { @@ -36,19 +48,22 @@ const History: React.FC = () => { fetchUrl(); }, []); - // Get all modifications + // Get modifications with pagination useEffect(() => { - fetchModifications(); - }, []); + fetchModifications(currentPage); + }, [currentPage]); - const fetchModifications = async () => { + const fetchModifications = async (page: number) => { + setLoading(true); try { - const response = await fetch('/api/modifications'); + const response = await fetch(`/api/modifications?page=${page}&pageSize=${pageSize}`); if (!response.ok) { throw new Error('Failed to fetch modifications'); } - const data = await response.json(); - setModifications(data); + const data: PaginatedResponse = await response.json(); + setModifications(data.items); + setTotalPages(data.totalPages); + setTotalItems(data.totalItems); } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error occurred'); } finally { @@ -96,7 +111,7 @@ const History: React.FC = () => { } return ( -
+

Modification History

@@ -108,19 +123,55 @@ const History: React.FC = () => { No modifications found

) : ( -
- {modifications.map((modification) => ( - - ))} -
+ <> +
+ {modifications.map((modification) => ( + + ))} +
+
+
+ + Showing {((currentPage - 1) * pageSize) + 1} to {Math.min(currentPage * pageSize, totalItems)} of {totalItems} results + +
+
+ + + Page {currentPage} of {totalPages} + + +
+
+ )}
); }; -export default History; \ No newline at end of file +export default History; diff --git a/web-app/src/components/DocumentCard.tsx b/web-app/src/components/DocumentCard.tsx index 1412fbe..5024c6b 100644 --- a/web-app/src/components/DocumentCard.tsx +++ b/web-app/src/components/DocumentCard.tsx @@ -6,7 +6,7 @@ interface DocumentCardProps { } const DocumentCard: React.FC = ({ document }) => ( -
+

{document.title}

{document.content.length > 100 diff --git a/web-app/src/components/SuccessModal.tsx b/web-app/src/components/SuccessModal.tsx index e06ce25..f3313e9 100644 --- a/web-app/src/components/SuccessModal.tsx +++ b/web-app/src/components/SuccessModal.tsx @@ -11,7 +11,7 @@ const SuccessModal: React.FC = ({ isOpen, onClose }) => (

diff --git a/web-app/src/components/SuggestionsReview.tsx b/web-app/src/components/SuggestionsReview.tsx index a0cc5af..c331c4b 100644 --- a/web-app/src/components/SuggestionsReview.tsx +++ b/web-app/src/components/SuggestionsReview.tsx @@ -25,7 +25,7 @@ const SuggestionsReview: React.FC = ({ onUpdate, updating, }) => ( -
+

Review and Edit Suggested Titles

diff --git a/web-app/src/components/UndoCard.tsx b/web-app/src/components/UndoCard.tsx index d3c2254..b131471 100644 --- a/web-app/src/components/UndoCard.tsx +++ b/web-app/src/components/UndoCard.tsx @@ -1,6 +1,6 @@ // UndoCard.tsx import React from 'react'; -import { Tooltip } from 'react-tooltip' +import { Tooltip } from 'react-tooltip'; interface ModificationProps { ID: number; @@ -72,7 +72,7 @@ const UndoCard: React.FC = ({ }; return ( -
+
{/* Left content */}