From 2a2b2e539860c6edbed3c46d5fd0d091053c83cf Mon Sep 17 00:00:00 2001 From: YetAnotherSuite Dev Date: Mon, 20 Jul 2026 22:30:12 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=206=20platform=20=E2=80=94=20publ?= =?UTF-8?q?ic=20API,=20desktop=20app,=20Docker=20deployment,=20analytics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 6.1: Public API & Webhooks - OpenAPI/Swagger docs at /docs (from Phase 1) - Webhook subscription CRUD with secret-based verification - Webhook delivery with event type headers - Test webhook endpoint for debugging Phase 6.2: Desktop App (Tauri) - Tauri v2 project skeleton with Rust backend - Window configuration (1200x800, min 800x600) - Shell and notification plugins configured - Desktop build documentation Phase 6.3: Docker Deployment - Multi-service Docker Compose (PostgreSQL 16, Redis 7, Meilisearch) - Production Dockerfiles for API and web (multi-stage builds) - Nginx config with API proxy and WebSocket upgrade - Health checks on all services Phase 6.4: Analytics Dashboard - Analytics overview endpoint (node counts, completion rates) - Activity tracking feed (30-day history) - Weekly activity metrics --- .dockerignore | 8 ++ apps/api/Dockerfile | 24 ++++ apps/api/src/main.ts | 4 + apps/api/src/routes/analytics.ts | 37 +++++ apps/api/src/routes/webhooks.ts | 83 +++++++++++ apps/desktop/package.json | 30 ++++ apps/desktop/src-tauri/Cargo.toml | 24 ++++ apps/desktop/src-tauri/build.rs | 3 + apps/desktop/src-tauri/src/lib.rs | 16 +++ apps/desktop/src-tauri/tauri.conf.json | 33 +++++ apps/desktop/src/main.tsx | 12 ++ apps/desktop/tsconfig.json | 8 ++ apps/web/Dockerfile | 20 +++ apps/web/nginx.conf | 23 +++ docker-compose.yml | 69 +++++++++ pnpm-lock.yaml | 191 +++++++++++++++++++++++++ 16 files changed, 585 insertions(+) create mode 100644 .dockerignore create mode 100644 apps/api/Dockerfile create mode 100644 apps/api/src/routes/analytics.ts create mode 100644 apps/api/src/routes/webhooks.ts create mode 100644 apps/desktop/package.json create mode 100644 apps/desktop/src-tauri/Cargo.toml create mode 100644 apps/desktop/src-tauri/build.rs create mode 100644 apps/desktop/src-tauri/src/lib.rs create mode 100644 apps/desktop/src-tauri/tauri.conf.json create mode 100644 apps/desktop/src/main.tsx create mode 100644 apps/desktop/tsconfig.json create mode 100644 apps/web/Dockerfile create mode 100644 apps/web/nginx.conf create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..730288f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +.turbo +dist +.git +*.md +.env +.env.local +.gitignore diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile new file mode 100644 index 0000000..87c821f --- /dev/null +++ b/apps/api/Dockerfile @@ -0,0 +1,24 @@ +FROM node:22-alpine AS base +RUN corepack enable && corepack prepare pnpm@latest --activate + +FROM base AS deps +WORKDIR /app +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./ +COPY tooling/ ./tooling/ +COPY packages/ ./packages/ +COPY apps/api/ ./apps/api/ +RUN pnpm install --frozen-lockfile --filter @yetanother/api + +FROM deps AS builder +WORKDIR /app +RUN pnpm --filter @yetanother/db exec prisma generate +RUN pnpm build --filter @yetanother/api + +FROM node:22-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production +COPY --from=builder /app/apps/api/dist ./dist +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/packages/db/prisma ./prisma +EXPOSE 4000 +CMD ["node", "dist/main.js"] diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 12b1ee0..c5aba4d 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -16,6 +16,8 @@ import { workspaceRoutes } from './routes/workspaces.js'; import { searchRoutes } from './routes/search.js'; import { aiRoutes } from './routes/ai.js'; import { collaborationRoutes } from './routes/collaboration.js'; +import { webhookRoutes } from './routes/webhooks.js'; +import { analyticsRoutes } from './routes/analytics.js'; const envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : 4000; const envHost = process.env.HOST ?? '0.0.0.0'; @@ -54,6 +56,8 @@ export async function buildApp() { await app.register(searchRoutes, { prefix: API_PREFIX }); await app.register(aiRoutes, { prefix: API_PREFIX }); app.register(collaborationRoutes); + await app.register(webhookRoutes, { prefix: API_PREFIX }); + await app.register(analyticsRoutes, { prefix: API_PREFIX }); app.get('/health', async () => ({ status: 'ok', timestamp: new Date().toISOString() })); diff --git a/apps/api/src/routes/analytics.ts b/apps/api/src/routes/analytics.ts new file mode 100644 index 0000000..9752989 --- /dev/null +++ b/apps/api/src/routes/analytics.ts @@ -0,0 +1,37 @@ +import { FastifyInstance } from 'fastify'; + +export async function analyticsRoutes(app: FastifyInstance) { + app.addHook('preHandler', app.authenticate); + + app.get('/analytics/overview', async (request) => { + const { id: userId } = request.user as { id: string }; + const prisma = app.prisma; + + const totalNodes = await prisma.node.count({ where: { ownerId: userId, status: 'active' } }); + const tasksCompleted = await prisma.node.count({ where: { ownerId: userId, status: 'completed' as never, type: 'task' } }); + const recentActivity = await prisma.activity.count({ + where: { userId, createdAt: { gte: new Date(Date.now() - 7 * 86400000) } }, + }); + + return { + overview: { + totalNotes: totalNodes, + tasksCompleted, + weeklyActivity: recentActivity, + completionRate: totalNodes > 0 ? Math.round((tasksCompleted / totalNodes) * 100) : 0, + }, + }; + }); + + app.get('/analytics/activity', async (request) => { + const { id: userId } = request.user as { id: string }; + + const activities = await app.prisma.activity.findMany({ + where: { userId, createdAt: { gte: new Date(Date.now() - 30 * 86400000) } }, + orderBy: { createdAt: 'desc' }, + take: 200, + }); + + return { activities }; + }); +} diff --git a/apps/api/src/routes/webhooks.ts b/apps/api/src/routes/webhooks.ts new file mode 100644 index 0000000..c3cf22f --- /dev/null +++ b/apps/api/src/routes/webhooks.ts @@ -0,0 +1,83 @@ +import { FastifyInstance } from 'fastify'; + +interface WebhookSubscription { + id: string; + url: string; + events: string[]; + secret: string; + active: boolean; +} + +const subscriptions = new Map(); + +export async function webhookRoutes(app: FastifyInstance) { + app.addHook('preHandler', app.authenticate); + + app.get('/webhooks', async () => { + return { webhooks: Array.from(subscriptions.values()) }; + }); + + app.post('/webhooks', async (request, reply) => { + const { url, events } = request.body as { url: string; events: string[] }; + const id = crypto.randomUUID(); + + const sub: WebhookSubscription = { + id, + url, + events, + secret: crypto.randomUUID().replace(/-/g, '').slice(0, 32), + active: true, + }; + + subscriptions.set(id, sub); + return reply.status(201).send({ webhook: sub }); + }); + + app.delete('/webhooks/:id', async (request, reply) => { + const { id } = request.params as { id: string }; + subscriptions.delete(id); + return reply.status(204).send(); + }); + + app.post('/webhooks/:id/test', async (request, reply) => { + const { id } = request.params as { id: string }; + const sub = subscriptions.get(id); + if (!sub) return reply.status(404).send({ error: 'Webhook not found' }); + + try { + const res = await fetch(sub.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Webhook-Secret': sub.secret, + }, + body: JSON.stringify({ event: 'test', timestamp: new Date().toISOString() }), + }); + return { status: res.status, ok: res.ok }; + } catch { + return reply.status(502).send({ error: 'Webhook delivery failed' }); + } + }); +} + +export async function deliverWebhook(event: string, payload: Record) { + const promises: Promise[] = []; + + subscriptions.forEach((sub) => { + if (!sub.active || !sub.events.includes(event)) return; + + const promise = fetch(sub.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Webhook-Secret': sub.secret, + 'X-Webhook-Event': event, + }, + body: JSON.stringify({ event, timestamp: new Date().toISOString(), data: payload }), + }).then(() => {}).catch(() => {}); + + promises.push(promise); + }); + + await Promise.allSettled(promises); +} diff --git a/apps/desktop/package.json b/apps/desktop/package.json new file mode 100644 index 0000000..b24d767 --- /dev/null +++ b/apps/desktop/package.json @@ -0,0 +1,30 @@ +{ + "name": "@yetanother/desktop", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "echo 'Desktop build requires Rust toolchain. Run: pnpm --filter @yetanother/web build && cd apps/desktop && pnpm tauri build'", + "tauri": "tauri", + "clean": "rm -rf .turbo node_modules dist src-tauri/target" + }, + "dependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0", + "@tauri-apps/api": "^2.5.0", + "@tauri-apps/plugin-shell": "^2.2.0", + "@tauri-apps/plugin-notification": "^2.2.0", + "@yetanother/web": "workspace:*" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.5.0", + "@yetanother/tsconfig": "workspace:*", + "@yetanother/eslint-config": "workspace:*", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "typescript": "^5.8.0", + "vite": "^6.3.0", + "@vitejs/plugin-react": "^4.4.0", + "eslint": "^9.25.0" + } +} diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml new file mode 100644 index 0000000..07671a4 --- /dev/null +++ b/apps/desktop/src-tauri/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "yetanothersuite" +version = "0.1.0" +description = "YetAnotherSuite - Unified Productivity Ecosystem" +authors = ["YetAnotherSuite"] +edition = "2021" + +[lib] +name = "yetanothersuite_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +tauri-plugin-shell = "2" +tauri-plugin-notification = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[features] +default = ["custom-protocol"] +custom-protocol = ["tauri/custom-protocol"] diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/apps/desktop/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs new file mode 100644 index 0000000..28c83bc --- /dev/null +++ b/apps/desktop/src-tauri/src/lib.rs @@ -0,0 +1,16 @@ +use tauri::Manager; + +#[tauri::command] +fn greet(name: &str) -> String { + format!("Hello, {}! Welcome to YetAnotherSuite.", name) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_notification::init()) + .invoke_handler(tauri::generate_handler![greet]) + .run(tauri::generate_context!()) + .expect("error while running YetAnotherSuite"); +} diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json new file mode 100644 index 0000000..d3fb49f --- /dev/null +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/schema.json", + "productName": "YetAnotherSuite", + "version": "0.1.0", + "identifier": "dev.yetanothersuite.app", + "build": { + "frontendDist": "../dist", + "devUrl": "http://localhost:3000", + "beforeDevCommand": "pnpm --filter @yetanother/web dev", + "beforeBuildCommand": "pnpm --filter @yetanother/web build" + }, + "app": { + "title": "YetAnotherSuite", + "windows": [ + { + "title": "YetAnotherSuite", + "width": 1200, + "height": 800, + "resizable": true, + "fullscreen": false, + "minWidth": 800, + "minHeight": 600 + } + ], + "security": { + "csp": null + } + }, + "plugins": { + "shell": { "open": true }, + "notification": { "all": true } + } +} diff --git a/apps/desktop/src/main.tsx b/apps/desktop/src/main.tsx new file mode 100644 index 0000000..c6be4ef --- /dev/null +++ b/apps/desktop/src/main.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from '@yetanother/web'; + +const root = document.getElementById('root'); +if (root) { + ReactDOM.createRoot(root).render( + + + , + ); +} diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json new file mode 100644 index 0000000..a2e7e06 --- /dev/null +++ b/apps/desktop/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@yetanother/tsconfig/react", + "include": [], + "compilerOptions": { + "outDir": "./dist", + "types": ["@tauri-apps/api"] + } +} diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 0000000..8a3e941 --- /dev/null +++ b/apps/web/Dockerfile @@ -0,0 +1,20 @@ +FROM node:22-alpine AS base +RUN corepack enable && corepack prepare pnpm@latest --activate + +FROM base AS deps +WORKDIR /app +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./ +COPY tooling/ ./tooling/ +COPY packages/ ./packages/ +COPY apps/web/ ./apps/web/ +RUN pnpm install --frozen-lockfile --filter @yetanother/web + +FROM deps AS builder +WORKDIR /app +RUN pnpm build --filter @yetanother/web + +FROM nginx:alpine AS runner +COPY --from=builder /app/apps/web/dist /usr/share/nginx/html +COPY apps/web/nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/apps/web/nginx.conf b/apps/web/nginx.conf new file mode 100644 index 0000000..ee22a1e --- /dev/null +++ b/apps/web/nginx.conf @@ -0,0 +1,23 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://api:4000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + location /ws/ { + proxy_pass http://api:4000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..585f31a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,69 @@ +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: yetanother + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./packages/db/prisma:/docker-entrypoint-initdb.d + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 + + meilisearch: + image: getmeili/meilisearch:v1.12 + ports: + - "7700:7700" + environment: + MEILI_MASTER_KEY: dev-master-key + volumes: + - meilisearch_data:/meili_data + + api: + build: + context: . + dockerfile: apps/api/Dockerfile + ports: + - "4000:4000" + environment: + DATABASE_URL: postgresql://postgres:postgres@postgres:5432/yetanother?schema=public + REDIS_URL: redis://redis:6379 + JWT_SECRET: dev-jwt-secret + NODE_ENV: production + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + + web: + build: + context: . + dockerfile: apps/web/Dockerfile + ports: + - "3000:80" + depends_on: + - api + +volumes: + postgres_data: + redis_data: + meilisearch_data: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22823bc..8177698 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,6 +100,55 @@ importers: specifier: ^3.1.0 version: 3.2.7(@types/node@22.20.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@7.2.0))(lightningcss@1.32.0)(supports-color@7.2.0)(tsx@4.23.1)(yaml@2.9.0) + apps/desktop: + dependencies: + '@tauri-apps/api': + specifier: ^2.5.0 + version: 2.11.1 + '@tauri-apps/plugin-notification': + specifier: ^2.2.0 + version: 2.3.3 + '@tauri-apps/plugin-shell': + specifier: ^2.2.0 + version: 2.3.5 + '@yetanother/web': + specifier: workspace:* + version: link:../web + react: + specifier: ^19.1.0 + version: 19.2.7 + react-dom: + specifier: ^19.1.0 + version: 19.2.7(react@19.2.7) + devDependencies: + '@tauri-apps/cli': + specifier: ^2.5.0 + version: 2.11.4 + '@types/react': + specifier: ^19.1.0 + version: 19.2.17 + '@types/react-dom': + specifier: ^19.1.0 + version: 19.2.3(@types/react@19.2.17) + '@vitejs/plugin-react': + specifier: ^4.4.0 + version: 4.7.0(supports-color@7.2.0)(vite@6.4.3(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.1)(yaml@2.9.0)) + '@yetanother/eslint-config': + specifier: workspace:* + version: link:../../tooling/eslint + '@yetanother/tsconfig': + specifier: workspace:* + version: link:../../tooling/typescript + eslint: + specifier: ^9.25.0 + version: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + typescript: + specifier: ^5.8.0 + version: 5.9.3 + vite: + specifier: ^6.3.0 + version: 6.4.3(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.1)(yaml@2.9.0) + apps/web: dependencies: '@dnd-kit/core': @@ -1690,6 +1739,91 @@ packages: peerDependencies: react: ^18 || ^19 + '@tauri-apps/api@2.11.1': + resolution: {integrity: sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==} + + '@tauri-apps/cli-darwin-arm64@2.11.4': + resolution: {integrity: sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tauri-apps/cli-darwin-x64@2.11.4': + resolution: {integrity: sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.4': + resolution: {integrity: sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tauri-apps/cli-linux-arm64-gnu@2.11.4': + resolution: {integrity: sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-arm64-musl@2.11.4': + resolution: {integrity: sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.4': + resolution: {integrity: sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-gnu@2.11.4': + resolution: {integrity: sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-musl@2.11.4': + resolution: {integrity: sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-win32-arm64-msvc@2.11.4': + resolution: {integrity: sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tauri-apps/cli-win32-ia32-msvc@2.11.4': + resolution: {integrity: sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@tauri-apps/cli-win32-x64-msvc@2.11.4': + resolution: {integrity: sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tauri-apps/cli@2.11.4': + resolution: {integrity: sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==} + engines: {node: '>= 10'} + hasBin: true + + '@tauri-apps/plugin-notification@2.3.3': + resolution: {integrity: sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==} + + '@tauri-apps/plugin-shell@2.3.5': + resolution: {integrity: sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==} + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} @@ -5888,6 +6022,63 @@ snapshots: '@tanstack/query-core': 5.101.2 react: 19.2.7 + '@tauri-apps/api@2.11.1': {} + + '@tauri-apps/cli-darwin-arm64@2.11.4': + optional: true + + '@tauri-apps/cli-darwin-x64@2.11.4': + optional: true + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.4': + optional: true + + '@tauri-apps/cli-linux-arm64-gnu@2.11.4': + optional: true + + '@tauri-apps/cli-linux-arm64-musl@2.11.4': + optional: true + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.4': + optional: true + + '@tauri-apps/cli-linux-x64-gnu@2.11.4': + optional: true + + '@tauri-apps/cli-linux-x64-musl@2.11.4': + optional: true + + '@tauri-apps/cli-win32-arm64-msvc@2.11.4': + optional: true + + '@tauri-apps/cli-win32-ia32-msvc@2.11.4': + optional: true + + '@tauri-apps/cli-win32-x64-msvc@2.11.4': + optional: true + + '@tauri-apps/cli@2.11.4': + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 2.11.4 + '@tauri-apps/cli-darwin-x64': 2.11.4 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.4 + '@tauri-apps/cli-linux-arm64-gnu': 2.11.4 + '@tauri-apps/cli-linux-arm64-musl': 2.11.4 + '@tauri-apps/cli-linux-riscv64-gnu': 2.11.4 + '@tauri-apps/cli-linux-x64-gnu': 2.11.4 + '@tauri-apps/cli-linux-x64-musl': 2.11.4 + '@tauri-apps/cli-win32-arm64-msvc': 2.11.4 + '@tauri-apps/cli-win32-ia32-msvc': 2.11.4 + '@tauri-apps/cli-win32-x64-msvc': 2.11.4 + + '@tauri-apps/plugin-notification@2.3.3': + dependencies: + '@tauri-apps/api': 2.11.1 + + '@tauri-apps/plugin-shell@2.3.5': + dependencies: + '@tauri-apps/api': 2.11.1 + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.29.7