From 221701d8082f792fbdd9b3a9498115670dd4da53 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Tue, 10 Feb 2026 18:30:35 +0530 Subject: [PATCH 1/5] feat: add Playwright e2e testing setup with CI workflow Set up end-to-end testing infrastructure using Playwright based on the wiki app patterns. Includes auth setup, Frappe API helpers, VMS-specific test helpers, an initial projects test suite, and a GitHub Actions workflow for automated UI testing. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ui-tests.yml | 153 +++++++++ .gitignore | 7 +- e2e/helpers/auth.ts | 63 ++++ e2e/helpers/frappe.ts | 226 +++++++++++++ e2e/helpers/index.ts | 3 + e2e/helpers/vms.ts | 152 +++++++++ e2e/pages/index.ts | 1 + e2e/tests/auth.setup.ts | 49 +++ e2e/tests/projects.spec.ts | 98 ++++++ e2e/tsconfig.json | 21 ++ package.json | 5 + playwright.config.ts | 42 +++ yarn.lock | 600 +++++++++++++++++++++++++++++++++ 13 files changed, 1419 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ui-tests.yml create mode 100644 e2e/helpers/auth.ts create mode 100644 e2e/helpers/frappe.ts create mode 100644 e2e/helpers/index.ts create mode 100644 e2e/helpers/vms.ts create mode 100644 e2e/pages/index.ts create mode 100644 e2e/tests/auth.setup.ts create mode 100644 e2e/tests/projects.spec.ts create mode 100644 e2e/tsconfig.json create mode 100644 playwright.config.ts create mode 100644 yarn.lock diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml new file mode 100644 index 0000000..76a294f --- /dev/null +++ b/.github/workflows/ui-tests.yml @@ -0,0 +1,153 @@ +name: UI Tests + +on: + push: + branches: [develop] + pull_request: + workflow_dispatch: + +concurrency: + group: ui-tests-vms-${{ github.event.number || github.ref }} + cancel-in-progress: true + +jobs: + ui-tests: + runs-on: ubuntu-latest + timeout-minutes: 60 + name: Playwright E2E Tests + + services: + redis-cache: + image: redis:alpine + ports: + - 13000:6379 + redis-queue: + image: redis:alpine + ports: + - 11000:6379 + mariadb: + image: mariadb:10.6 + env: + MYSQL_ROOT_PASSWORD: root + ports: + - 3306:3306 + options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + steps: + - name: Clone + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.14" + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 24 + check-latest: true + + - name: Add to Hosts + run: echo "127.0.0.1 vms.test" | sudo tee -a /etc/hosts + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + + - name: Cache yarn + uses: actions/cache@v4 + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('**/package.json') }} + + - name: Install MariaDB Client + run: | + sudo apt update + sudo apt-get install mariadb-client + + - name: Setup Bench + run: | + pip install frappe-bench + bench init --skip-redis-config-generation --skip-assets --python "$(which python)" ~/frappe-bench + mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" + mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" + + - name: Install VMS + working-directory: /home/runner/frappe-bench + run: | + bench get-app vms $GITHUB_WORKSPACE + bench setup requirements --dev + bench new-site --db-root-password root --admin-password admin vms.test + bench --site vms.test install-app vms + bench build + env: + CI: "Yes" + + - name: Configure Site + working-directory: /home/runner/frappe-bench + run: | + bench --site vms.test set-config allow_tests true + bench --site vms.test set-config host_name "http://vms.test:8000" + + - name: Start Frappe Server + working-directory: /home/runner/frappe-bench + run: | + sed -i 's/^watch:/# watch:/g' Procfile + sed -i 's/^schedule:/# schedule:/g' Procfile + bench start &> bench_start.log & + echo "Waiting for Frappe server to start..." + timeout 60 bash -c 'until curl -s http://vms.test:8000 > /dev/null; do sleep 2; done' + echo "Frappe server is ready!" + + - name: Install Playwright + run: | + cd $GITHUB_WORKSPACE + npm install + npx playwright install --with-deps chromium + + - name: Run Playwright Tests + working-directory: ${{ github.workspace }} + run: npx playwright test + env: + BASE_URL: http://vms.test:8000 + FRAPPE_USER: Administrator + FRAPPE_PASSWORD: admin + + - name: Upload Playwright Report + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 + + - name: Upload Test Results + uses: actions/upload-artifact@v4 + if: failure() + with: + name: test-results + path: test-results/ + retention-days: 7 + + - name: Show Bench Logs on Failure + if: failure() + working-directory: /home/runner/frappe-bench + run: | + echo "=== Bench Start Log ===" + cat bench_start.log || true + echo "" + echo "=== Frappe Logs ===" + cat logs/*.log || true diff --git a/.gitignore b/.gitignore index 8fe8be0..01c049b 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,9 @@ jspm_packages/ # Aider AI Chat .aider* -vms/public/frontend/assets \ No newline at end of file +vms/public/frontend/assets + +# Playwright +e2e/.auth/ +test-results/ +playwright-report/ \ No newline at end of file diff --git a/e2e/helpers/auth.ts b/e2e/helpers/auth.ts new file mode 100644 index 0000000..64c6758 --- /dev/null +++ b/e2e/helpers/auth.ts @@ -0,0 +1,63 @@ +import { APIRequestContext, Page } from "@playwright/test"; + +/** + * Login via Frappe API (faster than UI login). + * Sets cookies on the request context for subsequent API calls. + */ +export async function loginViaAPI( + request: APIRequestContext, + email = "Administrator", + password = "admin", +): Promise { + const response = await request.post("/api/method/login", { + form: { + usr: email, + pwd: password, + }, + }); + + if (!response.ok()) { + throw new Error(`Login failed: ${response.status()} ${await response.text()}`); + } +} + +/** + * Login via UI (for testing the login flow itself). + */ +export async function loginViaUI( + page: Page, + email = "Administrator", + password = "admin", +): Promise { + await page.goto("/login"); + await page.waitForLoadState("networkidle"); + + await page.fill('input[data-fieldname="email"]', email); + await page.fill('input[data-fieldname="password"]', password); + await page.click('button[type="submit"]'); + + await page.waitForURL(/\/(app|desk)/, { timeout: 30000 }); +} + +/** + * Logout the current user. + */ +export async function logout(page: Page): Promise { + await page.goto("/api/method/logout"); + await page.waitForLoadState("networkidle"); +} + +/** + * Check if user is logged in by verifying session. + */ +export async function isLoggedIn(request: APIRequestContext): Promise { + try { + const response = await request.get("/api/method/frappe.auth.get_logged_user"); + if (!response.ok()) return false; + + const data = await response.json(); + return data.message && data.message !== "Guest"; + } catch { + return false; + } +} diff --git a/e2e/helpers/frappe.ts b/e2e/helpers/frappe.ts new file mode 100644 index 0000000..20f085f --- /dev/null +++ b/e2e/helpers/frappe.ts @@ -0,0 +1,226 @@ +import * as fs from "fs"; +import { APIRequestContext } from "@playwright/test"; + +/** + * Frappe API response wrapper. + */ +export interface FrappeResponse { + message?: T; + exc?: string; + exc_type?: string; + _server_messages?: string; +} + +// Path to CSRF token file saved by auth.setup.ts +const CSRF_FILE = "e2e/.auth/csrf.json"; + +// Cache for CSRF token (read from file once) +let csrfTokenCache: string | null = null; + +/** + * Get CSRF token from the file saved during auth setup. + */ +function getCsrfToken(): string { + if (csrfTokenCache !== null) { + return csrfTokenCache; + } + + try { + if (fs.existsSync(CSRF_FILE)) { + const data = JSON.parse(fs.readFileSync(CSRF_FILE, "utf-8")); + csrfTokenCache = data.csrf_token || ""; + return csrfTokenCache; + } + } catch (error) { + console.warn("Failed to read CSRF token file:", error); + } + + csrfTokenCache = ""; + return ""; +} + +/** + * Create a new document via Frappe REST API. + */ +export async function createDoc>( + request: APIRequestContext, + doctype: string, + doc: Record, +): Promise { + const csrfToken = getCsrfToken(); + + const response = await request.post(`/api/resource/${doctype}`, { + data: doc, + headers: { + "Content-Type": "application/json", + ...(csrfToken ? { "X-Frappe-CSRF-Token": csrfToken } : {}), + }, + }); + + if (!response.ok()) { + const error = await response.text(); + throw new Error(`Failed to create ${doctype}: ${error}`); + } + + const result = await response.json(); + return result.data as T; +} + +/** + * Get a document by name via Frappe REST API. + */ +export async function getDoc>( + request: APIRequestContext, + doctype: string, + name: string, +): Promise { + const response = await request.get( + `/api/resource/${doctype}/${encodeURIComponent(name)}`, + ); + + if (!response.ok()) { + const error = await response.text(); + throw new Error(`Failed to get ${doctype}/${name}: ${error}`); + } + + const result = await response.json(); + return result.data as T; +} + +/** + * Update a document via Frappe REST API. + */ +export async function updateDoc>( + request: APIRequestContext, + doctype: string, + name: string, + updates: Record, +): Promise { + const csrfToken = getCsrfToken(); + + const response = await request.put( + `/api/resource/${doctype}/${encodeURIComponent(name)}`, + { + data: updates, + headers: { + "Content-Type": "application/json", + ...(csrfToken ? { "X-Frappe-CSRF-Token": csrfToken } : {}), + }, + }, + ); + + if (!response.ok()) { + const error = await response.text(); + throw new Error(`Failed to update ${doctype}/${name}: ${error}`); + } + + const result = await response.json(); + return result.data as T; +} + +/** + * Delete a document via Frappe REST API. + */ +export async function deleteDoc( + request: APIRequestContext, + doctype: string, + name: string, +): Promise { + const csrfToken = getCsrfToken(); + + const response = await request.delete( + `/api/resource/${doctype}/${encodeURIComponent(name)}`, + { + headers: { + ...(csrfToken ? { "X-Frappe-CSRF-Token": csrfToken } : {}), + }, + }, + ); + + if (!response.ok()) { + const error = await response.text(); + throw new Error(`Failed to delete ${doctype}/${name}: ${error}`); + } +} + +/** + * Call a Frappe whitelisted method. + */ +export async function callMethod( + request: APIRequestContext, + method: string, + args: Record = {}, +): Promise { + const csrfToken = getCsrfToken(); + + const response = await request.post(`/api/method/${method}`, { + data: args, + headers: { + "Content-Type": "application/json", + ...(csrfToken ? { "X-Frappe-CSRF-Token": csrfToken } : {}), + }, + }); + + if (!response.ok()) { + const error = await response.text(); + throw new Error(`Failed to call ${method}: ${error}`); + } + + const result: FrappeResponse = await response.json(); + return result.message as T; +} + +/** + * Get a list of documents via Frappe REST API. + */ +export async function getList>( + request: APIRequestContext, + doctype: string, + options: { + fields?: string[]; + filters?: Record; + limit?: number; + orderBy?: string; + } = {}, +): Promise { + const params = new URLSearchParams(); + + if (options.fields) { + params.set("fields", JSON.stringify(options.fields)); + } + if (options.filters) { + params.set("filters", JSON.stringify(options.filters)); + } + if (options.limit) { + params.set("limit_page_length", options.limit.toString()); + } + if (options.orderBy) { + params.set("order_by", options.orderBy); + } + + const response = await request.get(`/api/resource/${doctype}?${params.toString()}`); + + if (!response.ok()) { + const error = await response.text(); + throw new Error(`Failed to get list of ${doctype}: ${error}`); + } + + const result = await response.json(); + return result.data as T[]; +} + +/** + * Check if a document exists. + */ +export async function docExists( + request: APIRequestContext, + doctype: string, + name: string, +): Promise { + try { + await getDoc(request, doctype, name); + return true; + } catch { + return false; + } +} diff --git a/e2e/helpers/index.ts b/e2e/helpers/index.ts new file mode 100644 index 0000000..d7abda6 --- /dev/null +++ b/e2e/helpers/index.ts @@ -0,0 +1,3 @@ +export * from "./auth"; +export * from "./frappe"; +export * from "./vms"; diff --git a/e2e/helpers/vms.ts b/e2e/helpers/vms.ts new file mode 100644 index 0000000..2e68c2a --- /dev/null +++ b/e2e/helpers/vms.ts @@ -0,0 +1,152 @@ +import { APIRequestContext } from "@playwright/test"; +import { createDoc, deleteDoc, getDoc, getList } from "./frappe"; + +/** + * VMS Project document interface. + */ +export interface VMSProject { + name: string; + project_name: string; + status: string; + owner_user?: string; + due_date?: string; + description?: string; + creation?: string; + modified?: string; +} + +/** + * VMS Asset document interface. + */ +export interface VMSAsset { + name: string; + project: string; + file_name: string; + category: string; + status: string; + r2_key?: string; + file_size?: number; + file_type?: string; + uploaded_by?: string; + creation?: string; + modified?: string; +} + +/** + * Generate a unique project name for tests. + */ +export function generateProjectName(prefix = "E2E Test Project"): string { + const timestamp = Date.now(); + const random = Math.random().toString(36).substring(2, 8); + return `${prefix} ${timestamp}-${random}`; +} + +/** + * Create a test VMS Project via API. + */ +export async function createTestProject( + request: APIRequestContext, + options: { + project_name?: string; + status?: string; + description?: string; + } = {}, +): Promise { + const project_name = options.project_name || generateProjectName(); + + return createDoc(request, "VMS Project", { + project_name, + status: options.status ?? "Open", + description: options.description || `Test project: ${project_name}`, + }); +} + +/** + * Delete a test VMS Project via API. + */ +export async function deleteTestProject( + request: APIRequestContext, + name: string, +): Promise { + await deleteDoc(request, "VMS Project", name); +} + +/** + * Get a VMS Project by name via API. + */ +export async function getProject( + request: APIRequestContext, + name: string, +): Promise { + return getDoc(request, "VMS Project", name); +} + +/** + * List VMS Projects via API. + */ +export async function listProjects( + request: APIRequestContext, + options: { + filters?: Record; + limit?: number; + } = {}, +): Promise { + return getList(request, "VMS Project", { + fields: ["name", "project_name", "status", "owner_user", "creation"], + filters: options.filters, + limit: options.limit, + }); +} + +/** + * Create a test VMS Asset via API. + */ +export async function createTestAsset( + request: APIRequestContext, + options: { + project: string; + file_name?: string; + category?: string; + status?: string; + }, +): Promise { + const file_name = options.file_name || `test-video-${Date.now()}.mp4`; + + return createDoc(request, "VMS Asset", { + project: options.project, + file_name, + category: options.category ?? "Source", + status: options.status ?? "Ready", + }); +} + +/** + * Delete a test VMS Asset via API. + */ +export async function deleteTestAsset( + request: APIRequestContext, + name: string, +): Promise { + await deleteDoc(request, "VMS Asset", name); +} + +/** + * Cleanup test projects matching a name pattern. + */ +export async function cleanupTestProjects( + request: APIRequestContext, + namePattern = "E2E Test Project", +): Promise { + const projects = await listProjects(request, { + filters: { project_name: ["like", `${namePattern}%`] }, + limit: 100, + }); + + for (const project of projects) { + try { + await deleteTestProject(request, project.name); + } catch (error) { + console.warn(`Failed to delete ${project.name}:`, error); + } + } +} diff --git a/e2e/pages/index.ts b/e2e/pages/index.ts new file mode 100644 index 0000000..ac582ec --- /dev/null +++ b/e2e/pages/index.ts @@ -0,0 +1 @@ +// Page objects - add as needed diff --git a/e2e/tests/auth.setup.ts b/e2e/tests/auth.setup.ts new file mode 100644 index 0000000..e2657f8 --- /dev/null +++ b/e2e/tests/auth.setup.ts @@ -0,0 +1,49 @@ +import * as fs from "fs"; +import * as path from "path"; +import { expect, test as setup } from "@playwright/test"; + +const authFile = "e2e/.auth/user.json"; +const csrfFile = "e2e/.auth/csrf.json"; + +/** + * Authentication setup - runs once before all tests. + */ +setup("authenticate", async ({ page }) => { + const authDir = path.dirname(authFile); + if (!fs.existsSync(authDir)) { + fs.mkdirSync(authDir, { recursive: true }); + } + + // Login via Frappe API + const loginResponse = await page.request.post("/api/method/login", { + form: { + usr: process.env.FRAPPE_USER || "Administrator", + pwd: process.env.FRAPPE_PASSWORD || "admin", + }, + }); + expect(loginResponse.ok()).toBeTruthy(); + + // Verify login + const userResponse = await page.request.get( + "/api/method/frappe.auth.get_logged_user", + ); + expect(userResponse.ok()).toBeTruthy(); + const userData = await userResponse.json(); + expect(userData.message).not.toBe("Guest"); + console.log(`Authenticated as: ${userData.message}`); + + // Navigate to app to get CSRF token + await page.goto("/app"); + await page.waitForLoadState("networkidle"); + + const csrfToken = await page.evaluate(() => { + return (window as unknown as { frappe?: { csrf_token?: string } }).frappe + ?.csrf_token; + }); + + if (csrfToken) { + fs.writeFileSync(csrfFile, JSON.stringify({ csrf_token: csrfToken })); + } + + await page.context().storageState({ path: authFile }); +}); diff --git a/e2e/tests/projects.spec.ts b/e2e/tests/projects.spec.ts new file mode 100644 index 0000000..c41c8de --- /dev/null +++ b/e2e/tests/projects.spec.ts @@ -0,0 +1,98 @@ +import { test, expect } from "@playwright/test"; +import { + createTestProject, + deleteTestProject, + cleanupTestProjects, + VMSProject, +} from "../helpers/vms"; + +test.describe("Projects", () => { + let testProject: VMSProject; + + test.afterAll(async ({ request }) => { + await cleanupTestProjects(request); + }); + + test("should display the projects page", async ({ page }) => { + await page.goto("/vms/projects"); + await page.waitForLoadState("networkidle"); + + // The projects page should load + await expect(page).toHaveURL(/\/vms\/projects/); + }); + + test("should create a new project via UI", async ({ page }) => { + await page.goto("/vms/projects"); + await page.waitForLoadState("networkidle"); + + // Click the new project button + const newProjectBtn = page.locator('button:has-text("New Project"), button:has-text("Create Project"), [data-testid="new-project"]'); + await expect(newProjectBtn.first()).toBeVisible({ timeout: 10000 }); + await newProjectBtn.first().click(); + + // Fill in the project name + const projectName = `E2E Test Project ${Date.now()}`; + const nameInput = page.locator('input[placeholder*="project" i], input[name="project_name"], [data-fieldname="project_name"] input'); + await expect(nameInput.first()).toBeVisible({ timeout: 5000 }); + await nameInput.first().fill(projectName); + + // Submit the form + const submitBtn = page.locator('button:has-text("Create"), button[type="submit"]'); + await submitBtn.first().click(); + + // Should navigate to the project detail page or show success + await page.waitForLoadState("networkidle"); + + // Verify project was created by checking we're on a project page or it appears in the list + await page.goto("/vms/projects"); + await page.waitForLoadState("networkidle"); + await expect(page.locator(`text=${projectName}`).first()).toBeVisible({ timeout: 10000 }); + }); + + test("should create a project via API and see it in the list", async ({ page, request }) => { + testProject = await createTestProject(request, { + project_name: `E2E Test Project API ${Date.now()}`, + }); + + await page.goto("/vms/projects"); + await page.waitForLoadState("networkidle"); + + // The API-created project should appear in the list + await expect( + page.locator(`text=${testProject.project_name}`).first(), + ).toBeVisible({ timeout: 10000 }); + }); + + test("should navigate to project detail page", async ({ page, request }) => { + if (!testProject) { + testProject = await createTestProject(request, { + project_name: `E2E Test Project Nav ${Date.now()}`, + }); + } + + await page.goto("/vms/projects"); + await page.waitForLoadState("networkidle"); + + // Click on the project + await page.locator(`text=${testProject.project_name}`).first().click(); + + // Should navigate to the project detail page + await page.waitForLoadState("networkidle"); + await expect(page).toHaveURL(/\/vms\/projects\//); + }); + + test("should delete a project via API", async ({ request }) => { + const tempProject = await createTestProject(request, { + project_name: `E2E Test Project Delete ${Date.now()}`, + }); + + // Delete and verify + await deleteTestProject(request, tempProject.name); + + // Verify it's gone + const response = await request.get( + `/api/resource/VMS Project/${encodeURIComponent(tempProject.name)}`, + ); + expect(response.status()).toBe(404); + }); +}); diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json new file mode 100644 index 0000000..77bf0e7 --- /dev/null +++ b/e2e/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "noEmit": true, + "baseUrl": ".", + "paths": { + "@helpers/*": ["helpers/*"], + "@pages/*": ["pages/*"] + } + }, + "include": ["**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/package.json b/package.json index e55638b..bc4bb0b 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,10 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:headed": "playwright test --headed", + "test:e2e:debug": "playwright test --debug", "postinstall": "cd frontend && yarn install", "dev": "cd frontend && yarn dev", "build": "cd frontend && yarn build" @@ -14,6 +18,7 @@ "license": "ISC", "type": "commonjs", "devDependencies": { + "@playwright/test": "^1.58.2", "wrangler": "^4.63.0" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..0be799a --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,42 @@ +import { defineConfig, devices } from "@playwright/test"; +import path from "path"; + +const authFile = path.join(__dirname, "e2e", ".auth", "user.json"); + +export default defineConfig({ + testDir: "./e2e/tests", + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, + reporter: process.env.CI ? [["github"], ["html", { open: "never" }]] : "html", + timeout: 60000, + + expect: { + timeout: 10000, + }, + + use: { + baseURL: process.env.BASE_URL || "http://vms.test:8000", + trace: "on-first-retry", + video: "retain-on-failure", + screenshot: "only-on-failure", + actionTimeout: 15000, + navigationTimeout: 30000, + }, + + projects: [ + { + name: "setup", + testMatch: /auth\.setup\.ts/, + }, + { + name: "chromium", + use: { + ...devices["Desktop Chrome"], + storageState: authFile, + }, + dependencies: ["setup"], + }, + ], +}); diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..1e5da7d --- /dev/null +++ b/yarn.lock @@ -0,0 +1,600 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@cloudflare/kv-asset-handler@0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz#b6b8eab81f0f9d8378e219dd321df20280e3bbd2" + integrity sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ== + +"@cloudflare/unenv-preset@2.12.1": + version "2.12.1" + resolved "https://registry.yarnpkg.com/@cloudflare/unenv-preset/-/unenv-preset-2.12.1.tgz#6d4e67983f4cbca30415439151e397721b285c49" + integrity sha512-tP/Wi+40aBJovonSNJSsS7aFJY0xjuckKplmzDs2Xat06BJ68B6iG7YDUWXJL8gNn0gqW7YC5WhlYhO3QbugQA== + +"@cloudflare/workerd-darwin-64@1.20260210.0": + version "1.20260210.0" + resolved "https://registry.yarnpkg.com/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260210.0.tgz#d5b5144f7a8c4218a78f3bda572912a732450be5" + integrity sha512-e3vMgzr8ZM6VjpJVFrnMBhjvFhlMIkhT+BLpBk3pKaWsrXao+azDlmzzxB3Zf4CZ8LmCEtaP7n5d2mNGL6Dqww== + +"@cloudflare/workerd-darwin-arm64@1.20260210.0": + version "1.20260210.0" + resolved "https://registry.yarnpkg.com/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260210.0.tgz#4710e844f76e817a29e34eecf57a69615dc33b1f" + integrity sha512-ng2uLJVMrI5VrcAS26gDGM+qxCuWD4ZA8VR4i88RdyM8TLn+AqPFisrvn7AMA+QSv0+ck+ZdFtXek7qNp2gNuA== + +"@cloudflare/workerd-linux-64@1.20260210.0": + version "1.20260210.0" + resolved "https://registry.yarnpkg.com/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260210.0.tgz#8e5f99641098f7abc88c09177daa09f4d5ca39d7" + integrity sha512-frn2/+6DV59h13JbGSk9ATvJw3uORWssFIKZ/G/to+WRrIDQgCpSrjLtGbFSSn5eBEhYOvwxPKc7IrppkmIj/w== + +"@cloudflare/workerd-linux-arm64@1.20260210.0": + version "1.20260210.0" + resolved "https://registry.yarnpkg.com/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260210.0.tgz#8fcf775d0e373cd41110b89d5049ccf8662af9c8" + integrity sha512-0fmxEHaDcAF+7gcqnBcQdBCOzNvGz3mTMwqxEYJc5xZgFwQf65/dYK5fnV8z56GVNqu88NEnLMG3DD2G7Ey1vw== + +"@cloudflare/workerd-windows-64@1.20260210.0": + version "1.20260210.0" + resolved "https://registry.yarnpkg.com/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260210.0.tgz#48b917ca0873b08dfdd0cad07839cb65c133acf6" + integrity sha512-G/Apjk/QLNnwbu8B0JO9FuAJKHNr+gl8X3G/7qaUrpwIkPx5JFQElVE6LKk4teSrycvAy5AzLFAL0lOB1xsUIQ== + +"@cspotcode/source-map-support@0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@emnapi/runtime@^1.7.0": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.8.1.tgz#550fa7e3c0d49c5fb175a116e8cd70614f9a22a5" + integrity sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg== + dependencies: + tslib "^2.4.0" + +"@esbuild/aix-ppc64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz#815b39267f9bffd3407ea6c376ac32946e24f8d2" + integrity sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg== + +"@esbuild/android-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz#19b882408829ad8e12b10aff2840711b2da361e8" + integrity sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg== + +"@esbuild/android-arm@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.3.tgz#90be58de27915efa27b767fcbdb37a4470627d7b" + integrity sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA== + +"@esbuild/android-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.3.tgz#d7dcc976f16e01a9aaa2f9b938fbec7389f895ac" + integrity sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ== + +"@esbuild/darwin-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz#9f6cac72b3a8532298a6a4493ed639a8988e8abd" + integrity sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg== + +"@esbuild/darwin-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz#ac61d645faa37fd650340f1866b0812e1fb14d6a" + integrity sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg== + +"@esbuild/freebsd-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz#b8625689d73cf1830fe58c39051acdc12474ea1b" + integrity sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w== + +"@esbuild/freebsd-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz#07be7dd3c9d42fe0eccd2ab9f9ded780bc53bead" + integrity sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA== + +"@esbuild/linux-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz#bf31918fe5c798586460d2b3d6c46ed2c01ca0b6" + integrity sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg== + +"@esbuild/linux-arm@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz#28493ee46abec1dc3f500223cd9f8d2df08f9d11" + integrity sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw== + +"@esbuild/linux-ia32@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz#750752a8b30b43647402561eea764d0a41d0ee29" + integrity sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg== + +"@esbuild/linux-loong64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz#a5a92813a04e71198c50f05adfaf18fc1e95b9ed" + integrity sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA== + +"@esbuild/linux-mips64el@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz#deb45d7fd2d2161eadf1fbc593637ed766d50bb1" + integrity sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw== + +"@esbuild/linux-ppc64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz#6f39ae0b8c4d3d2d61a65b26df79f6e12a1c3d78" + integrity sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA== + +"@esbuild/linux-riscv64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz#4c5c19c3916612ec8e3915187030b9df0b955c1d" + integrity sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ== + +"@esbuild/linux-s390x@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz#9ed17b3198fa08ad5ccaa9e74f6c0aff7ad0156d" + integrity sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw== + +"@esbuild/linux-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz#12383dcbf71b7cf6513e58b4b08d95a710bf52a5" + integrity sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA== + +"@esbuild/netbsd-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz#dd0cb2fa543205fcd931df44f4786bfcce6df7d7" + integrity sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA== + +"@esbuild/netbsd-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz#028ad1807a8e03e155153b2d025b506c3787354b" + integrity sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA== + +"@esbuild/openbsd-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz#e3c16ff3490c9b59b969fffca87f350ffc0e2af5" + integrity sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw== + +"@esbuild/openbsd-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz#c5a4693fcb03d1cbecbf8b422422468dfc0d2a8b" + integrity sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ== + +"@esbuild/openharmony-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz#082082444f12db564a0775a41e1991c0e125055e" + integrity sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g== + +"@esbuild/sunos-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz#5ab036c53f929e8405c4e96e865a424160a1b537" + integrity sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA== + +"@esbuild/win32-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz#38de700ef4b960a0045370c171794526e589862e" + integrity sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA== + +"@esbuild/win32-ia32@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz#451b93dc03ec5d4f38619e6cd64d9f9eff06f55c" + integrity sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q== + +"@esbuild/win32-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz#0eaf705c941a218a43dba8e09f1df1d6cd2f1f17" + integrity sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA== + +"@img/colour@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@img/colour/-/colour-1.0.0.tgz#d2fabb223455a793bf3bf9c70de3d28526aa8311" + integrity sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw== + +"@img/sharp-darwin-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz#6e0732dcade126b6670af7aa17060b926835ea86" + integrity sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w== + optionalDependencies: + "@img/sharp-libvips-darwin-arm64" "1.2.4" + +"@img/sharp-darwin-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz#19bc1dd6eba6d5a96283498b9c9f401180ee9c7b" + integrity sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw== + optionalDependencies: + "@img/sharp-libvips-darwin-x64" "1.2.4" + +"@img/sharp-libvips-darwin-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz#2894c0cb87d42276c3889942e8e2db517a492c43" + integrity sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g== + +"@img/sharp-libvips-darwin-x64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz#e63681f4539a94af9cd17246ed8881734386f8cc" + integrity sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg== + +"@img/sharp-libvips-linux-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz#b1b288b36864b3bce545ad91fa6dadcf1a4ad318" + integrity sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw== + +"@img/sharp-libvips-linux-arm@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz#b9260dd1ebe6f9e3bdbcbdcac9d2ac125f35852d" + integrity sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A== + +"@img/sharp-libvips-linux-ppc64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz#4b83ecf2a829057222b38848c7b022e7b4d07aa7" + integrity sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA== + +"@img/sharp-libvips-linux-riscv64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz#880b4678009e5a2080af192332b00b0aaf8a48de" + integrity sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA== + +"@img/sharp-libvips-linux-s390x@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz#74f343c8e10fad821b38f75ced30488939dc59ec" + integrity sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ== + +"@img/sharp-libvips-linux-x64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz#df4183e8bd8410f7d61b66859a35edeab0a531ce" + integrity sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw== + +"@img/sharp-libvips-linuxmusl-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz#c8d6b48211df67137541007ee8d1b7b1f8ca8e06" + integrity sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw== + +"@img/sharp-libvips-linuxmusl-x64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz#be11c75bee5b080cbee31a153a8779448f919f75" + integrity sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg== + +"@img/sharp-linux-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz#7aa7764ef9c001f15e610546d42fce56911790cc" + integrity sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg== + optionalDependencies: + "@img/sharp-libvips-linux-arm64" "1.2.4" + +"@img/sharp-linux-arm@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz#5fb0c3695dd12522d39c3ff7a6bc816461780a0d" + integrity sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw== + optionalDependencies: + "@img/sharp-libvips-linux-arm" "1.2.4" + +"@img/sharp-linux-ppc64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz#9c213a81520a20caf66978f3d4c07456ff2e0813" + integrity sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA== + optionalDependencies: + "@img/sharp-libvips-linux-ppc64" "1.2.4" + +"@img/sharp-linux-riscv64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz#cdd28182774eadbe04f62675a16aabbccb833f60" + integrity sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw== + optionalDependencies: + "@img/sharp-libvips-linux-riscv64" "1.2.4" + +"@img/sharp-linux-s390x@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz#93eac601b9f329bb27917e0e19098c722d630df7" + integrity sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg== + optionalDependencies: + "@img/sharp-libvips-linux-s390x" "1.2.4" + +"@img/sharp-linux-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz#55abc7cd754ffca5002b6c2b719abdfc846819a8" + integrity sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ== + optionalDependencies: + "@img/sharp-libvips-linux-x64" "1.2.4" + +"@img/sharp-linuxmusl-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz#d6515ee971bb62f73001a4829b9d865a11b77086" + integrity sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-arm64" "1.2.4" + +"@img/sharp-linuxmusl-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz#d97978aec7c5212f999714f2f5b736457e12ee9f" + integrity sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-x64" "1.2.4" + +"@img/sharp-wasm32@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz#2f15803aa626f8c59dd7c9d0bbc766f1ab52cfa0" + integrity sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw== + dependencies: + "@emnapi/runtime" "^1.7.0" + +"@img/sharp-win32-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz#3706e9e3ac35fddfc1c87f94e849f1b75307ce0a" + integrity sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g== + +"@img/sharp-win32-ia32@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz#0b71166599b049e032f085fb9263e02f4e4788de" + integrity sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg== + +"@img/sharp-win32-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz#a81ffb00e69267cd0a1d626eaedb8a8430b2b2f8" + integrity sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw== + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@playwright/test@^1.58.2": + version "1.58.2" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.58.2.tgz#b0ad585d2e950d690ef52424967a42f40c6d2cbd" + integrity sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA== + dependencies: + playwright "1.58.2" + +"@poppinss/colors@^4.1.5": + version "4.1.6" + resolved "https://registry.yarnpkg.com/@poppinss/colors/-/colors-4.1.6.tgz#bf8546e30cfc5ee8dfe68988ce58eb0ad9d7c21b" + integrity sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg== + dependencies: + kleur "^4.1.5" + +"@poppinss/dumper@^0.6.4": + version "0.6.5" + resolved "https://registry.yarnpkg.com/@poppinss/dumper/-/dumper-0.6.5.tgz#8992703338d80d2218fdc37245c8cfc67f0f6ac9" + integrity sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw== + dependencies: + "@poppinss/colors" "^4.1.5" + "@sindresorhus/is" "^7.0.2" + supports-color "^10.0.0" + +"@poppinss/exception@^1.2.2": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@poppinss/exception/-/exception-1.2.3.tgz#b713855e6c9fe2110fea0949455c50828145e64a" + integrity sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw== + +"@sindresorhus/is@^7.0.2": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-7.2.0.tgz#7c594e1a64336d2008d99d814056d459421504d4" + integrity sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw== + +"@speed-highlight/core@^1.2.7": + version "1.2.14" + resolved "https://registry.yarnpkg.com/@speed-highlight/core/-/core-1.2.14.tgz#5d7fe87410d2d779bd0b7680f7a706466f363314" + integrity sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA== + +blake3-wasm@2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/blake3-wasm/-/blake3-wasm-2.1.5.tgz#b22dbb84bc9419ed0159caa76af4b1b132e6ba52" + integrity sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g== + +cookie@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.1.1.tgz#3bb9bdfc82369db9c2f69c93c9c3ceb310c88b3c" + integrity sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ== + +detect-libc@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + +error-stack-parser-es@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz#e6a1655dd12f39bb3a85bf4c7088187d78740327" + integrity sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA== + +esbuild@0.27.3: + version "0.27.3" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.3.tgz#5859ca8e70a3af956b26895ce4954d7e73bd27a8" + integrity sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg== + optionalDependencies: + "@esbuild/aix-ppc64" "0.27.3" + "@esbuild/android-arm" "0.27.3" + "@esbuild/android-arm64" "0.27.3" + "@esbuild/android-x64" "0.27.3" + "@esbuild/darwin-arm64" "0.27.3" + "@esbuild/darwin-x64" "0.27.3" + "@esbuild/freebsd-arm64" "0.27.3" + "@esbuild/freebsd-x64" "0.27.3" + "@esbuild/linux-arm" "0.27.3" + "@esbuild/linux-arm64" "0.27.3" + "@esbuild/linux-ia32" "0.27.3" + "@esbuild/linux-loong64" "0.27.3" + "@esbuild/linux-mips64el" "0.27.3" + "@esbuild/linux-ppc64" "0.27.3" + "@esbuild/linux-riscv64" "0.27.3" + "@esbuild/linux-s390x" "0.27.3" + "@esbuild/linux-x64" "0.27.3" + "@esbuild/netbsd-arm64" "0.27.3" + "@esbuild/netbsd-x64" "0.27.3" + "@esbuild/openbsd-arm64" "0.27.3" + "@esbuild/openbsd-x64" "0.27.3" + "@esbuild/openharmony-arm64" "0.27.3" + "@esbuild/sunos-x64" "0.27.3" + "@esbuild/win32-arm64" "0.27.3" + "@esbuild/win32-ia32" "0.27.3" + "@esbuild/win32-x64" "0.27.3" + +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +kleur@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" + integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== + +miniflare@4.20260210.0: + version "4.20260210.0" + resolved "https://registry.yarnpkg.com/miniflare/-/miniflare-4.20260210.0.tgz#a8e34e9978e490747c00b62dd276fe7ca03c4a55" + integrity sha512-HXR6m53IOqEzq52DuGF1x7I1K6lSIqzhbCbQXv/cTmPnPJmNkr7EBtLDm4nfSkOvlDtnwDCLUjWII5fyGJI5Tw== + dependencies: + "@cspotcode/source-map-support" "0.8.1" + sharp "^0.34.5" + undici "7.18.2" + workerd "1.20260210.0" + ws "8.18.0" + youch "4.1.0-beta.10" + +path-to-regexp@6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4" + integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ== + +pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + +playwright-core@1.58.2: + version "1.58.2" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.58.2.tgz#ac5f5b4b10d29bcf934415f0b8d133b34b0dcb13" + integrity sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg== + +playwright@1.58.2: + version "1.58.2" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.58.2.tgz#afe547164539b0bcfcb79957394a7a3fa8683cfd" + integrity sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A== + dependencies: + playwright-core "1.58.2" + optionalDependencies: + fsevents "2.3.2" + +semver@^7.7.3: + version "7.7.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" + integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== + +sharp@^0.34.5: + version "0.34.5" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.5.tgz#b6f148e4b8c61f1797bde11a9d1cfebbae2c57b0" + integrity sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg== + dependencies: + "@img/colour" "^1.0.0" + detect-libc "^2.1.2" + semver "^7.7.3" + optionalDependencies: + "@img/sharp-darwin-arm64" "0.34.5" + "@img/sharp-darwin-x64" "0.34.5" + "@img/sharp-libvips-darwin-arm64" "1.2.4" + "@img/sharp-libvips-darwin-x64" "1.2.4" + "@img/sharp-libvips-linux-arm" "1.2.4" + "@img/sharp-libvips-linux-arm64" "1.2.4" + "@img/sharp-libvips-linux-ppc64" "1.2.4" + "@img/sharp-libvips-linux-riscv64" "1.2.4" + "@img/sharp-libvips-linux-s390x" "1.2.4" + "@img/sharp-libvips-linux-x64" "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64" "1.2.4" + "@img/sharp-libvips-linuxmusl-x64" "1.2.4" + "@img/sharp-linux-arm" "0.34.5" + "@img/sharp-linux-arm64" "0.34.5" + "@img/sharp-linux-ppc64" "0.34.5" + "@img/sharp-linux-riscv64" "0.34.5" + "@img/sharp-linux-s390x" "0.34.5" + "@img/sharp-linux-x64" "0.34.5" + "@img/sharp-linuxmusl-arm64" "0.34.5" + "@img/sharp-linuxmusl-x64" "0.34.5" + "@img/sharp-wasm32" "0.34.5" + "@img/sharp-win32-arm64" "0.34.5" + "@img/sharp-win32-ia32" "0.34.5" + "@img/sharp-win32-x64" "0.34.5" + +supports-color@^10.0.0: + version "10.2.2" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-10.2.2.tgz#466c2978cc5cd0052d542a0b576461c2b802ebb4" + integrity sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g== + +tslib@^2.4.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +undici@7.18.2: + version "7.18.2" + resolved "https://registry.yarnpkg.com/undici/-/undici-7.18.2.tgz#6cf724ef799a67d94fd55adf66b1e184176efcdf" + integrity sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw== + +unenv@2.0.0-rc.24: + version "2.0.0-rc.24" + resolved "https://registry.yarnpkg.com/unenv/-/unenv-2.0.0-rc.24.tgz#dd0035c3e93fedfa12c8454e34b7f17fe83efa2e" + integrity sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw== + dependencies: + pathe "^2.0.3" + +workerd@1.20260210.0: + version "1.20260210.0" + resolved "https://registry.yarnpkg.com/workerd/-/workerd-1.20260210.0.tgz#f7d2f465b7ffec52e81d13f23ebfad8cdbc3e5e9" + integrity sha512-Sb0WXhrvf+XHQigP2trAxQnXo7wxZFC4PWnn6I7LhFxiTvzxvOAqMEiLkIz58wggRCb54T/KAA8hdjkTniR5FA== + optionalDependencies: + "@cloudflare/workerd-darwin-64" "1.20260210.0" + "@cloudflare/workerd-darwin-arm64" "1.20260210.0" + "@cloudflare/workerd-linux-64" "1.20260210.0" + "@cloudflare/workerd-linux-arm64" "1.20260210.0" + "@cloudflare/workerd-windows-64" "1.20260210.0" + +wrangler@^4.63.0: + version "4.64.0" + resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-4.64.0.tgz#58a94416cbcca6827f54efe3221c80757dd652f6" + integrity sha512-0PBiVEbshQT4Av/KLHbOAks4ioIKp/eAO7Xr2BgAX5v7cFYYgeOvudBrbtZa/hDDIA6858QuJnTQ8mI+cm8Vqw== + dependencies: + "@cloudflare/kv-asset-handler" "0.4.2" + "@cloudflare/unenv-preset" "2.12.1" + blake3-wasm "2.1.5" + esbuild "0.27.3" + miniflare "4.20260210.0" + path-to-regexp "6.3.0" + unenv "2.0.0-rc.24" + workerd "1.20260210.0" + optionalDependencies: + fsevents "~2.3.2" + +ws@8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + +youch-core@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/youch-core/-/youch-core-0.3.3.tgz#c5d3d85aeea0d8bc7b36e9764ed3f14b7ceddc7d" + integrity sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA== + dependencies: + "@poppinss/exception" "^1.2.2" + error-stack-parser-es "^1.0.5" + +youch@4.1.0-beta.10: + version "4.1.0-beta.10" + resolved "https://registry.yarnpkg.com/youch/-/youch-4.1.0-beta.10.tgz#94702059e0ba7668025f5cd1b5e5c0f3eb0e83c2" + integrity sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ== + dependencies: + "@poppinss/colors" "^4.1.5" + "@poppinss/dumper" "^0.6.4" + "@speed-highlight/core" "^1.2.7" + cookie "^1.0.2" + youch-core "^0.3.3" From 6af69011c152578c29337b24fa303acd1ed59719 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Tue, 10 Feb 2026 18:32:41 +0530 Subject: [PATCH 2/5] style: fix pre-commit formatting issues Run ruff-format and prettier on existing files to pass CI linter checks. Co-Authored-By: Claude Opus 4.6 --- frontend/eslint.config.js | 42 ++++++------ vms/api.py | 64 +++++++++++++------ vms/hooks.py | 6 +- vms/thumbnails.py | 36 ++++++----- .../vms_audit_log/test_vms_audit_log.py | 2 - .../doctype/vms_settings/test_vms_settings.py | 2 - 6 files changed, 92 insertions(+), 60 deletions(-) diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 5e6b472..1abf82e 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -1,23 +1,23 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; +import { defineConfig, globalIgnores } from "eslint/config"; export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - }, -]) + globalIgnores(["dist"]), + { + files: ["**/*.{ts,tsx}"], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]); diff --git a/vms/api.py b/vms/api.py index 3918914..1c7d1e4 100644 --- a/vms/api.py +++ b/vms/api.py @@ -1,9 +1,14 @@ -import requests - import frappe +import requests from frappe import _ -from vms.r2 import delete_r2_object, generate_presigned_download_url, generate_presigned_upload_url, generate_presigned_view_url, get_r2_client +from vms.r2 import ( + delete_r2_object, + generate_presigned_download_url, + generate_presigned_upload_url, + generate_presigned_view_url, + get_r2_client, +) @frappe.whitelist() @@ -79,7 +84,9 @@ def get_upload_url(file_name: str, content_type: str, project: str = None, categ # Validate category valid_categories = ("Source", "Cut", "Review", "Final") if category not in valid_categories: - frappe.throw(_("Invalid category '{0}'. Must be one of: {1}").format(category, ", ".join(valid_categories))) + frappe.throw( + _("Invalid category '{0}'. Must be one of: {1}").format(category, ", ".join(valid_categories)) + ) # Generate presigned URL upload_url, r2_key = generate_presigned_upload_url(file_name, content_type, project) @@ -131,20 +138,29 @@ def confirm_upload(asset_name: str, file_size: int): return {"status": "ok", "asset_name": asset.name} -def _create_audit_log(action: str, asset_name: str, file_name: str = None, file_type: str = None, project: str = None, file_size: int = None): +def _create_audit_log( + action: str, + asset_name: str, + file_name: str = None, + file_type: str = None, + project: str = None, + file_size: int = None, +): """Create an audit log entry. Never raises — failures are logged silently.""" try: - doc = frappe.get_doc({ - "doctype": "VMS Audit Log", - "action": action, - "asset_name": asset_name, - "user": frappe.session.user, - "timestamp": frappe.utils.now_datetime(), - "file_name": file_name, - "file_type": file_type, - "project": project, - "file_size": file_size, - }) + doc = frappe.get_doc( + { + "doctype": "VMS Audit Log", + "action": action, + "asset_name": asset_name, + "user": frappe.session.user, + "timestamp": frappe.utils.now_datetime(), + "file_name": file_name, + "file_type": file_type, + "project": project, + "file_size": file_size, + } + ) doc.insert(ignore_permissions=True) except Exception: frappe.logger("vms").warning(f"Failed to create audit log for {action} on {asset_name}") @@ -279,7 +295,9 @@ def update_asset_category(asset_name: str, category: str): """Change an asset's category (Source/Cut/Review/Final).""" valid_categories = ("Source", "Cut", "Review", "Final") if category not in valid_categories: - frappe.throw(_("Invalid category '{0}'. Must be one of: {1}").format(category, ", ".join(valid_categories))) + frappe.throw( + _("Invalid category '{0}'. Must be one of: {1}").format(category, ", ".join(valid_categories)) + ) asset = frappe.get_doc("VMS Asset", asset_name) asset.category = category @@ -323,7 +341,17 @@ def get_audit_logs( logs = frappe.get_all( "VMS Audit Log", filters=filters, - fields=["name", "action", "asset_name", "user", "timestamp", "file_name", "file_type", "project", "file_size"], + fields=[ + "name", + "action", + "asset_name", + "user", + "timestamp", + "file_name", + "file_type", + "project", + "file_size", + ], order_by="timestamp desc", start=start, page_length=page_size, diff --git a/vms/hooks.py b/vms/hooks.py index f93de90..ea91d8c 100644 --- a/vms/hooks.py +++ b/vms/hooks.py @@ -254,10 +254,12 @@ # ignore_translatable_strings_from = [] -website_route_rules = [{'from_route': '/vms/', 'to_route': 'vms'},] +website_route_rules = [ + {"from_route": "/vms/", "to_route": "vms"}, +] user_invitation = { "allowed_roles": { "System Manager": ["Video Manager"], }, -} \ No newline at end of file +} diff --git a/vms/thumbnails.py b/vms/thumbnails.py index 739266f..12d9b50 100644 --- a/vms/thumbnails.py +++ b/vms/thumbnails.py @@ -3,11 +3,10 @@ import subprocess import tempfile +import frappe import requests from PIL import Image -import frappe - from vms.r2 import generate_presigned_view_url IMAGE_MIME_PREFIXES = ("image/jpeg", "image/png", "image/webp", "image/gif", "image/bmp", "image/tiff") @@ -41,11 +40,16 @@ def _generate_video_thumbnail(video_path, thumb_path, asset_name): result = subprocess.run( [ "ffmpeg", - "-ss", "1", - "-i", video_path, - "-vframes", "1", - "-f", "image2", - "-q:v", "3", + "-ss", + "1", + "-i", + video_path, + "-vframes", + "1", + "-f", + "image2", + "-q:v", + "3", thumb_path, ], capture_output=True, @@ -92,14 +96,16 @@ def generate_thumbnail(asset_name): with open(thumb_path, "rb") as f: content = f.read() - file_doc = frappe.get_doc({ - "doctype": "File", - "file_name": f"{asset_name}.jpg", - "attached_to_doctype": "VMS Asset", - "attached_to_name": asset_name, - "content": content, - "is_private": 0, - }) + file_doc = frappe.get_doc( + { + "doctype": "File", + "file_name": f"{asset_name}.jpg", + "attached_to_doctype": "VMS Asset", + "attached_to_name": asset_name, + "content": content, + "is_private": 0, + } + ) file_doc.save(ignore_permissions=True) asset.reload() diff --git a/vms/video_management_solution/doctype/vms_audit_log/test_vms_audit_log.py b/vms/video_management_solution/doctype/vms_audit_log/test_vms_audit_log.py index e9013ed..ff976aa 100644 --- a/vms/video_management_solution/doctype/vms_audit_log/test_vms_audit_log.py +++ b/vms/video_management_solution/doctype/vms_audit_log/test_vms_audit_log.py @@ -4,7 +4,6 @@ # import frappe from frappe.tests import IntegrationTestCase - # On IntegrationTestCase, the doctype test records and all # link-field test record dependencies are recursively loaded # Use these module variables to add/remove to/from that list @@ -12,7 +11,6 @@ IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] - class IntegrationTestVMSAuditLog(IntegrationTestCase): """ Integration tests for VMSAuditLog. diff --git a/vms/video_management_solution/doctype/vms_settings/test_vms_settings.py b/vms/video_management_solution/doctype/vms_settings/test_vms_settings.py index 55a1d56..6d74bb7 100644 --- a/vms/video_management_solution/doctype/vms_settings/test_vms_settings.py +++ b/vms/video_management_solution/doctype/vms_settings/test_vms_settings.py @@ -4,7 +4,6 @@ # import frappe from frappe.tests import IntegrationTestCase - # On IntegrationTestCase, the doctype test records and all # link-field test record dependencies are recursively loaded # Use these module variables to add/remove to/from that list @@ -12,7 +11,6 @@ IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] - class IntegrationTestVMSSettings(IntegrationTestCase): """ Integration tests for VMSSettings. From 6a2c6b8807fb950276895ab5ea786e1b1ca93a44 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Tue, 10 Feb 2026 18:34:17 +0530 Subject: [PATCH 3/5] style: fix implicit Optional type annotations (RUF013) Convert `str = None` to `str | None = None` to satisfy ruff RUF013 rule. Co-Authored-By: Claude Opus 4.6 --- vms/api.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/vms/api.py b/vms/api.py index 1c7d1e4..648274e 100644 --- a/vms/api.py +++ b/vms/api.py @@ -63,7 +63,7 @@ def fail_upload(asset_name: str): @frappe.whitelist() -def get_upload_url(file_name: str, content_type: str, project: str = None, category: str = "Source"): +def get_upload_url(file_name: str, content_type: str, project: str | None = None, category: str = "Source"): """Generate a presigned upload URL for direct upload to R2. Returns dict with upload_url, r2_key, and asset_name. @@ -141,10 +141,10 @@ def confirm_upload(asset_name: str, file_size: int): def _create_audit_log( action: str, asset_name: str, - file_name: str = None, - file_type: str = None, - project: str = None, - file_size: int = None, + file_name: str | None = None, + file_type: str | None = None, + project: str | None = None, + file_size: int | None = None, ): """Create an audit log entry. Never raises — failures are logged silently.""" try: @@ -308,11 +308,11 @@ def update_asset_category(asset_name: str, category: str): @frappe.whitelist(methods=["GET"]) def get_audit_logs( - action: str = None, - user: str = None, - search: str = None, - from_date: str = None, - to_date: str = None, + action: str | None = None, + user: str | None = None, + search: str | None = None, + from_date: str | None = None, + to_date: str | None = None, page: int = 1, page_size: int = 20, ): From a11b69b59bbc406cda9b45e43a0b2f875106c635 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Tue, 10 Feb 2026 18:35:54 +0530 Subject: [PATCH 4/5] fix: exclude tsconfig JSONC files from check-json pre-commit hook TypeScript tsconfig files use JSONC (comments allowed) which fails strict JSON validation. Co-Authored-By: Claude Opus 4.6 --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 568a323..eca68ad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,6 +13,7 @@ repos: - id: check-merge-conflict - id: check-ast - id: check-json + exclude: "tsconfig.*\\.json$" - id: check-toml - id: check-yaml - id: debug-statements From f486c77bccf4d9ec50132f0e0f064d48d6dcc094 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Tue, 10 Feb 2026 18:40:17 +0530 Subject: [PATCH 5/5] fix: include mandatory owner_user field when creating test projects Co-Authored-By: Claude Opus 4.6 --- e2e/helpers/vms.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e/helpers/vms.ts b/e2e/helpers/vms.ts index 2e68c2a..40e0083 100644 --- a/e2e/helpers/vms.ts +++ b/e2e/helpers/vms.ts @@ -58,6 +58,7 @@ export async function createTestProject( project_name, status: options.status ?? "Open", description: options.description || `Test project: ${project_name}`, + owner_user: "Administrator", }); }