diff --git a/.gitea/workflows/analytics-mailer.yml b/.gitea/workflows/analytics-mailer.yml index 1622fc6..48bca8b 100644 --- a/.gitea/workflows/analytics-mailer.yml +++ b/.gitea/workflows/analytics-mailer.yml @@ -1,13 +1,25 @@ -name: Monthly Analytics Mailer +name: Analytics Mailer on: schedule: - - cron: '0 8 1 * *' # Run at 08:00 on the 1st of every month - workflow_dispatch: # Allow manual trigger + # Run weekly on Monday at 08:00 UTC + - cron: '0 8 * * 1' + # Run monthly on the 1st day of the month at 08:30 UTC + - cron: '30 8 1 * *' + workflow_dispatch: + inputs: + interval: + description: 'Interval to run (weekly or monthly)' + required: true + default: 'weekly' + type: choice + options: + - weekly + - monthly jobs: send-reports: - name: 📊 Send Monthly Reports + name: 📊 Send Analytics Reports runs-on: docker container: image: catthehacker/ubuntu:act-latest @@ -40,5 +52,18 @@ jobs: - name: Build analytics mailer run: pnpm --filter @mintel/analytics-mailer build + - name: Determine Interval + id: set-interval + run: | + if [ "${{ github.event_name }}" = "schedule" ]; then + if [ "${{ github.event.schedule }}" = "0 8 * * 1" ]; then + echo "interval=weekly" >> $GITHUB_OUTPUT + else + echo "interval=monthly" >> $GITHUB_OUTPUT + fi + else + echo "interval=${{ github.event.inputs.interval }}" >> $GITHUB_OUTPUT + fi + - name: Run Analytics Mailer - run: pnpm --filter @mintel/analytics-mailer run start + run: pnpm --filter @mintel/analytics-mailer run start -- --interval=${{ steps.set-interval.outputs.interval }} diff --git a/apps/analytics-mailer/config.json b/apps/analytics-mailer/config.json index 2b5b44a..e2a453f 100644 --- a/apps/analytics-mailer/config.json +++ b/apps/analytics-mailer/config.json @@ -1,14 +1,34 @@ [ - { - "websiteId": "d773ea10-a3b3-4ccf-9024-987e14c4d669", - "domain": "e-tib.com", - "clientEmail": "marc@mintel.me", - "clientName": "E-TIB GmbH" - }, { "websiteId": "59a7db94-0100-4c7e-98ef-99f45b17f9c3", "domain": "klz-cables.com", "clientEmail": "marc@mintel.me", - "clientName": "KLZ" + "clientName": "KLZ Cables", + "greeting": "Hallo Herr Künzel,", + "interval": "monthly" + }, + { + "websiteId": "b9035279-72f1-49bc-909d-b073d184c388", + "domain": "mintel.me", + "clientEmail": "marc@mintel.me", + "clientName": "Mintel.me", + "greeting": "Hallo Team Mintel,", + "interval": "monthly" + }, + { + "websiteId": "caa402bc-b2d2-4349-a254-84c0dd2a4460", + "domain": "mb-grid-solutions.com", + "clientEmail": "marc@mintel.me", + "clientName": "MB Grid Solutions", + "greeting": "Hallo Herr Bartenbach,", + "interval": "monthly" + }, + { + "websiteId": "d773ea10-a3b3-4ccf-9024-987e14c4d669", + "domain": "e-tib.com", + "clientEmail": "marc@mintel.me", + "clientName": "E-TIB GmbH", + "greeting": "Sehr geehrter Herr Mintel,", + "interval": "monthly" } ] diff --git a/apps/analytics-mailer/package.json b/apps/analytics-mailer/package.json index dffe03b..5007873 100644 --- a/apps/analytics-mailer/package.json +++ b/apps/analytics-mailer/package.json @@ -18,9 +18,10 @@ "devDependencies": { "@mintel/eslint-config": "workspace:*", "@mintel/tsconfig": "workspace:*", - "@types/node": "^22.10.2", + "@types/node": "^22.19.10", "tsup": "^8.3.5", "tsx": "^4.19.2", - "typescript": "^5.7.2" + "typescript": "^5.7.2", + "vitest": "^4.1.10" } } diff --git a/apps/analytics-mailer/src/index.ts b/apps/analytics-mailer/src/index.ts index 532712b..e5b8832 100644 --- a/apps/analytics-mailer/src/index.ts +++ b/apps/analytics-mailer/src/index.ts @@ -2,7 +2,8 @@ import fs from "fs/promises"; import path from "path"; import axios from "axios"; import { render } from "@mintel/mail"; -import { MonthlyAnalyticsTemplate } from "@mintel/mail"; +import { AnalyticsTemplate } from "@mintel/mail"; +import { getLast7Days, getThisMonth, getThisYear } from "./utils/time-calculator"; import Mailgun from "mailgun.js"; import formData from "form-data"; import * as dotenv from "dotenv"; @@ -24,6 +25,8 @@ interface ConfigClient { domain: string; clientEmail: string; clientName: string; + greeting?: string; + interval?: "weekly" | "monthly"; } async function getUmamiToken(): Promise { @@ -40,27 +43,67 @@ async function getUmamiToken(): Promise { return res.data.token; } -async function fetchUmamiData(api: any, websiteId: string, startAt: number, endAt: number) { - const [statsRes, pagesRes, refRes] = await Promise.all([ +import https from "https"; + +async function fetchUmamiData(api: any, websiteId: string, startAt: number, endAt: number, label: string) { + let unit = "day"; + if (label === "Dieses Jahr") unit = "month"; + + const [statsRes, pagesRes, refRes, viewsRes] = await Promise.all([ api.get(`/websites/${websiteId}/stats`, { params: { startAt, endAt } }), - api.get(`/websites/${websiteId}/metrics`, { params: { type: "url", limit: 5, startAt, endAt } }), - api.get(`/websites/${websiteId}/metrics`, { params: { type: "referrer", limit: 3, startAt, endAt } }), + api.get(`/websites/${websiteId}/metrics`, { params: { type: "path", limit: 5, startAt, endAt } }), + api.get(`/websites/${websiteId}/metrics`, { params: { type: "referrer", limit: 5, startAt, endAt } }), + api.get(`/websites/${websiteId}/pageviews`, { params: { startAt, endAt, unit, timezone: "Europe/Berlin" } }).catch(() => ({ data: { pageviews: [] } })), ]); const stats = statsRes.data; - const topPages = (pagesRes.data || []).map((p: any) => ({ url: p.x, views: p.y })); - const topReferrers = (refRes.data || []).map((r: any) => ({ url: r.x, visitors: r.y })); + const topPages = (pagesRes.data || []).map((p: any) => ({ url: p.x, views: p.y || 0 })); + const topReferrers = (refRes.data || []).map((r: any) => ({ url: r.x, visitors: r.y || 0 })); + + const visitors = typeof stats.visitors === "object" ? stats.visitors?.value || 0 : stats.visitors || 0; + const visits = typeof stats.visits === "object" ? stats.visits?.value || 0 : stats.visits || 0; + const views = typeof stats.pageviews === "object" ? stats.pageviews?.value || 0 : stats.pageviews || 0; + const bounces = typeof stats.bounces === "object" ? stats.bounces?.value || 0 : stats.bounces || 0; + const totaltime = typeof stats.totaltime === "object" ? stats.totaltime?.value || 0 : stats.totaltime || 0; + + const bounceRate = visits > 0 ? Math.round((bounces / visits) * 100) : 0; + + let visitDuration = "0s"; + if (visits > 0) { + const avgSeconds = Math.round(totaltime / visits); + if (avgSeconds < 60) { + visitDuration = `${avgSeconds}s`; + } else { + visitDuration = `${Math.floor(avgSeconds / 60)}m ${avgSeconds % 60}s`; + } + } + + const chartData = (viewsRes.data?.pageviews || []).map((pv: any) => ({ + label: pv.x, + value: pv.y || 0 + })); return { - totalVisitors: stats.visitors?.value || 0, - totalPageviews: stats.pageviews?.value || 0, + label, + visitors, + visits, + views, + bounceRate, + visitDuration, topPages, topReferrers, + chartData, }; } async function main() { const isDryRun = process.argv.includes("--dry-run"); + const intervalArg = process.argv.find(a => a.startsWith("--interval=")); + const runInterval = intervalArg ? intervalArg.split("=")[1] : "monthly"; + + if (runInterval !== "weekly" && runInterval !== "monthly") { + throw new Error("Invalid interval. Use --interval=weekly or --interval=monthly"); + } // Load config const configPath = path.resolve(process.cwd(), "config.json"); @@ -70,47 +113,39 @@ async function main() { // Setup Umami API const token = await getUmamiToken(); const headers = UMAMI_API_KEY ? { "x-umami-api-key": token } : { Authorization: `Bearer ${token}` }; - const api = axios.create({ baseURL: `${UMAMI_BASE_URL}/api`, headers }); + const httpsAgent = new https.Agent({ rejectUnauthorized: false }); + const api = axios.create({ baseURL: `${UMAMI_BASE_URL}/api`, headers, httpsAgent }); // Setup Mailgun const mailgun = new Mailgun(formData); - const mg = mailgun.client({ username: "api", key: MAILGUN_API_KEY!, url: MAILGUN_URL }); + const mg = mailgun.client({ username: "api", key: MAILGUN_API_KEY || "dummy", url: MAILGUN_URL }); - // Time range calculation (previous month) - const now = new Date(); - // Ensure we get the correct previous month relative to current local time, but use UTC boundary or local? Umami uses ms timestamps. - // Standard way: First day of previous month, last day of previous month. - const startOfPrevMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); - const endOfPrevMonth = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999); - - const startAt = startOfPrevMonth.getTime(); - const endAt = endOfPrevMonth.getTime(); + const timeRanges = [getLast7Days(), getThisMonth(), getThisYear()]; - const monthName = startOfPrevMonth.toLocaleString("de-DE", { month: "long" }); - const year = startOfPrevMonth.getFullYear().toString(); - - console.log(`Generating reports for ${monthName} ${year}...`); + console.log(`Generating reports...`); for (const client of clients) { if (client.websiteId === "replace-with-umami-website-id") continue; + const clientInterval = client.interval || "monthly"; + if (clientInterval !== runInterval) { + console.log(`Skipping ${client.domain} (runs ${clientInterval}, currently executing ${runInterval})`); + continue; + } console.log(`Processing ${client.domain} (${client.clientName})...`); try { - const data = await fetchUmamiData(api, client.websiteId, startAt, endAt); + const periods = await Promise.all( + timeRanges.map((tr) => fetchUmamiData(api, client.websiteId, tr.startAt, tr.endAt, tr.label)) + ); - const html = await render(MonthlyAnalyticsTemplate({ - clientName: client.clientName, + const html = await render(AnalyticsTemplate({ + greeting: client.greeting || `Hallo ${client.clientName},`, domain: client.domain, - month: monthName, - year, - totalVisitors: data.totalVisitors, - totalPageviews: data.totalPageviews, - topPages: data.topPages, - topReferrers: data.topReferrers, + periods, })); - const subject = `Ihr Website-Report für ${monthName} ${year} - ${client.domain}`; + const subject = `Performance-Report - ${client.domain}`; if (isDryRun) { console.log(`[DRY-RUN] Would send email to ${client.clientEmail} for ${client.domain}`); diff --git a/apps/analytics-mailer/src/utils/time-calculator.ts b/apps/analytics-mailer/src/utils/time-calculator.ts new file mode 100644 index 0000000..5939984 --- /dev/null +++ b/apps/analytics-mailer/src/utils/time-calculator.ts @@ -0,0 +1,19 @@ +export function getLast7Days() { + const endAt = Date.now(); + const startAt = endAt - 7 * 24 * 60 * 60 * 1000; + return { label: "Letzte 7 Tage", startAt, endAt }; +} + +export function getThisMonth() { + const now = new Date(); + const startAt = new Date(now.getFullYear(), now.getMonth(), 1).getTime(); + const endAt = Date.now(); + return { label: "Dieser Monat", startAt, endAt }; +} + +export function getThisYear() { + const now = new Date(); + const startAt = new Date(now.getFullYear(), 0, 1).getTime(); + const endAt = Date.now(); + return { label: "Dieses Jahr", startAt, endAt }; +} diff --git a/package.json b/package.json index 517a951..c2b37e3 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@next/eslint-plugin-next": "16.1.6", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", - "@types/node": "^20.17.16", + "@types/node": "^20.19.33", "@types/react": "^19.2.10", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.2", diff --git a/packages/mail/src/index.ts b/packages/mail/src/index.ts index 48328e4..eaa8d6e 100644 --- a/packages/mail/src/index.ts +++ b/packages/mail/src/index.ts @@ -25,4 +25,4 @@ export * from "./templates/ConfirmationMessage"; export * from "./templates/FollowUpTemplate"; export * from "./templates/ProjectEstimateTemplate"; export * from "./templates/SiteAuditTemplate"; -export * from "./templates/MonthlyAnalyticsTemplate"; +export * from "./templates/AnalyticsTemplate"; diff --git a/packages/mail/src/layouts/GatekeeperLayout.tsx b/packages/mail/src/layouts/GatekeeperLayout.tsx new file mode 100644 index 0000000..60aeea6 --- /dev/null +++ b/packages/mail/src/layouts/GatekeeperLayout.tsx @@ -0,0 +1,197 @@ +import { + Body, + Container, + Head, + Html, + Preview, + Section, + Text, + Img, +} from "@react-email/components"; +import * as React from "react"; + +export interface GatekeeperLayoutProps { + preview: string; + projectName?: string; + subTitle?: string; + children: React.ReactNode; +} + +export const GatekeeperLayout = ({ preview, projectName = "MINTEL", subTitle = "INFRASTRUCTURE", children }: GatekeeperLayoutProps) => { + return ( + + + {preview} + + + + {/* Top Icon Box */} +
+
+ Mintel +
+
+ + {/* Title Area */} +
+ + {projectName} GATEKEEPER + + + + + + + + + + +
+ + {subTitle} + +
+
+ + {/* Main Content Area */} +
+ {children} +
+ + {/* Footer Area */} +
+
+
+ {projectName} +
+ + © {new Date().getFullYear()} MINTEL + +
+ +
+ + + ); +}; + +const main = { + backgroundColor: "#f5f5f7", + color: "#111111", + fontFamily: + 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif', + WebkitFontSmoothing: "antialiased" as const, +}; + +const container = { + margin: "0 auto", + padding: "40px 20px 80px", + maxWidth: "420px", +}; + +const iconBoxSection = { + textAlign: "center" as const, + marginBottom: "40px", +}; + +const iconBox = { + width: "56px", + height: "56px", + backgroundColor: "#ffffff", + borderRadius: "16px", + margin: "0 auto", + display: "flex", + alignItems: "center", + justifyContent: "center", + border: "1px solid rgba(0,0,0,0.06)", + boxShadow: "0 10px 15px -3px rgba(0,0,0,0.06), 0 4px 6px -2px rgba(0,0,0,0.03)", +}; + +const iconImg = { + margin: "14px auto", + display: "block", + opacity: 0.8, + filter: "grayscale(100%) brightness(0)", +}; + +const titleSection = { + textAlign: "center" as const, + marginBottom: "32px", +}; + +const gatekeeperTitle = { + fontSize: "11px", + fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif', + fontWeight: "bold", + textTransform: "uppercase" as const, + letterSpacing: "0.5em", + color: "rgba(0,0,0,0.8)", + margin: "0 0 12px 0", +}; + +const gatekeeperSub = { + fontSize: "8px", + fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif', + textTransform: "uppercase" as const, + letterSpacing: "0.35em", + fontWeight: 600, + color: "rgba(0,0,0,0.3)", + margin: "0", +}; + +const lineGradientRight = { + height: "1px", + width: "32px", + backgroundColor: "rgba(0,0,0,0.1)", + marginLeft: "auto", +}; + +const lineGradientLeft = { + height: "1px", + width: "32px", + backgroundColor: "rgba(0,0,0,0.1)", + marginRight: "auto", +}; + +const contentSection = { + backgroundColor: "#ffffff", + padding: "32px", + borderRadius: "24px", + border: "1px solid rgba(0,0,0,0.06)", + boxShadow: "0 4px 6px -1px rgba(0,0,0,0.03)", + marginBottom: "32px", +}; + +const footerSection = { + textAlign: "center" as const, + paddingTop: "24px", +}; + +const lineGradientCenter = { + height: "1px", + width: "64px", + backgroundColor: "rgba(0,0,0,0.1)", + margin: "0 auto", +}; + +const footerText = { + fontSize: "7px", + fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif', + fontWeight: 600, + color: "rgba(0,0,0,0.25)", + textTransform: "uppercase" as const, + letterSpacing: "0.5em", + marginTop: "20px", +}; diff --git a/packages/mail/src/templates/AnalyticsTemplate.tsx b/packages/mail/src/templates/AnalyticsTemplate.tsx new file mode 100644 index 0000000..bb08320 --- /dev/null +++ b/packages/mail/src/templates/AnalyticsTemplate.tsx @@ -0,0 +1,318 @@ +import * as React from "react"; +import { Html, Head, Body, Container, Section, Text, Row, Column, Img } from "@react-email/components"; + +export interface PeriodData { + label: string; + visitors: number; + visits: number; + views: number; + bounceRate: number; + visitDuration: string; + topPages: { url: string; views: number }[]; + topReferrers: { url: string; visitors: number }[]; + chartData: { label: string; value: number }[]; +} + +export interface AnalyticsTemplateProps { + greeting: string; + domain: string; + periods: PeriodData[]; +} + +const logoBase64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAc4AAADFCAYAAADdVhLHAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAgAElEQVR4nO2dd9gU1dXAf/u+Lx2kCYqKiAoiYCxYIhq70RB7icYW9LP3Ho2999iIicYSNZbE3ksMdo3G9sWKoiAgRopKEaTu98d593tn7p3Znd2dmTu7e37Pcx9l35m5Z+q599xTciiKUg05YFlgFaAd0NTafgT+0/pfRVHqiJxrARSlxugDjAI2A4a0tl4h2y4C3gZeA24EPk1DQEVRFEVxSQ5YCzgdUYBLgXwFbR5wHDIbVRRFUZS6ognYBrgemEg0xbgUmAHMLLHdc0BLameiKIqiKAnSAzgBGE9x5bcAUYCnAOsDfYFmz3E6AP2BXwIPICZb7/7/k/ypKIqiKEpyrIWsQf5AuLL8CLgG+AXQpczjb4c4CRWONQXoFIfgiqIoipIWLcAewAuEK8vZiLIcFEN/hxjHPj6GYyqKoihK4vQFzgAmE64wxyOOPMvE2G9H/Ouft8Z4bEWpJVYDtkCjOhQl8wwD7kDWJ8MU5j+BnfCvV8bJ3zx9PZhQH4qSVVqAP9H2DlzjVhxFUcIYCtxL8TCSxxAnn6S50tPn2BT6U5Ss0Al4GP97NxOddSpKplgTuJtsKMwC53j6fjfFfhXFJT2Al7Dfv/tdCqUoShtrAHdRXGE+DmzgQLbbPDJ84qB/RUmbFZDUk+Y7+C2wukO5FEUBBgN3AksIV5gvABs7kg/gZfzKW1HqmcHABOz3cDGSYERRFEcMQpx+iinMd5BYStfrKV/TJtPljmVRlCQZAUwj+H081qFcitLQ9AKuQ0avYQrzM2AvspEftit+2UY7lUZRkmMbYA7B7+TNuB/AKkrD0QIcTfGcsF8BhyJlvrLCevhl3NCtOIqSCL8CFhL8Xr6KpKNUFCVFtgU+IFxhfgucCnR2JWARfodf1m5uxVGU2DmKcKe8ycBy7kRTlMZjdeARwhXmPOAixO09q7xDm7yTHMuiKHGSA86n+Pu5njPpFKXBWAZxogkz/eSBW4B+rgSMyKr4Zf6LU2kUJT6akQIJYe9nHjHfKoqSMM1I6a1vCH8Z36R21glPwS/7nm7FUZRY6IiUzSumNC9wJp2iNBDrAP8m/EWcBhxENjxlo/IGbfIvArq7FUdRqqY78DzFlebD1NZ7qig1RyfgEsLDSxYD15LtdcwgBuA/D81RqwSxMnAMsLNrQSKwPJIyspjS/AB1gFOURNkc+JTwl/B5YLgz6arjCvzncpJbcZQMsi0wA3k+lgJ7uxWnKKsBn1Ncac5E1vUVRUmAHsBNhL+AkxHHgloNmO6JHQg+xKlESpZoQmrDmiEcb7gUqgjrAv+luNJcDGzpSkBFqXd2BaYS/PItAC4EujiTLh7OwH9e71C7gwAlXnoi1XmCnv8jHMoVxhbAbIorzTxwpCP5FKWu6UdxT7yx1EfVhM7YuToPdSqRkhXWBb4g+Pm/0KFcYexG8QLwhXYjOjBUlFjJAQcD3xP80s1q/Xu9vHhH4T+/OaizhAIHAvMJfgeeAtq7Ey2QwyheQKHQXiJ7sitKTdOHcLNUHskKtKIz6eKnHTAR/zn+yaVAinM6Unw9/32ytTSRA86mtMLMI896HydSKkqdsjXha5nTqG3nnzD2xT7XdZ1KpLhkFeAtwhXPLKQ8XlZoBsYQ7Phj/vYDsLYbMRWl/miHxGWGJX2+A+jtTLrkyGFXvM+ql6SSPNtTvJJPHnGUywodgL9hy/gt9nOdB3Z3I6ai1B+r4s+W421fIh+TemUU9jnv41QixQVNwDmEDxwL7TJXAgbQCXgOW8bJwFUBv5/rREpFqUP2Idxt/Xrq30HmJfznPA4xfSmNQ2/E0afU2uBYpL5sVjgcW8aPkVml6VX7AJpOT1GqphtS9SPoAzEe2NSZZOmxCfa57+9UIiVtRmA7hgW1qWSvPuWl+GX8F5KwY7Lx+/8CXR3JqCh1wwjgM4I/EDfROC/Z49gDhizNKJRkORj4kdJKcwmSZjJrLI+YamcjMZndkVmxV/bpiLOToigV0oTkXg2qlzkD2MWdaKmzOfY1ONCpREpadELqwoYpSfO3s9yIGZnCYM/Ms7wI2MyVUIpSDywPPEPwx+Ipsl9cOk6asMMNJiCexUp9syqSStF8BxYDd2EPKp+jNta8f4V9Toc5lUhRapztCS4yPR/JmFNvcZml2A/7WhzgVCIlDX4JfId9779u/ZtZReQbZMCZdYYDc/HLfoNTiRSlhulAsFt6HqnPN9SdaM7oBEzCfy1eQz0O65lm4HyC34MXEWvL343flwLbuBC2THpgl/h7EbWeKEpFDAbexv5QLEW88Ro1T+Xp2NdjfacSKUmyLOFLFFciCiYorOMCF8KWSRPwKH65JwN9XQqlKLVIDhiNbbrJIzOtLHoHpkVf7JjVPzuVSEmSDbGtC3kkgf8erdusje1Z+xK14V19FvbSywinEik1STOSeLk3koR8NWAY8nIMRWZhA4GVkLWL3ogLdwcXwiZAd+BugkfX9yFmnUbmBvzX5Hs02XW9sivBpbU+BNZo3aYrkvDC9C6vhQIGo7CzHGkMcgi1MAqKg+7AAES59UKKyPYK+P/Cv7sh1Qyqset/i5g5zDaptX2JPJxZZQSiHAcavy8CTgT+QLblT5q1sL0Mz0bi3JT6ogW4Dns54l7gEMQakwP+iAymvRwAfJW0gFWyGuIB7HXquxq40404ShrkkAwcGwJ7Aicjqd0eRTJchNV/dN2+Rh7M35C9Eem+BNcMnAhs4E6szJADnsd/bT6gcQaijUYOcfgq3OtFwLH4Fc1o7Pfl8lSlrIwu2Mnb/4k+y3VFH2BL4Bgks8WrSEke10owjvYRMqrdCVgmrgtWJi2Ee80+gszGFRmgmddnS6cSKUnTD/g9khTALBE3FCmv5X0eXif7nqg5ZKbplXsC4gClFCGr8XYtSCzR+q3/HY6YxpLy7voBKf0zo/W/M5EF/0VIILP3v97/X4Ksa3hNvqbpt5KXZyGS9/Uy4IvKTqlseiOmJ9NlfgnwW+Sj0cim2QKdgU+A/p7f7kMCxpXGozNSCWi457fvEOX6pROJonMccI3n3/OBjRFLnVID9EIWpy9EzARBHpzltqXI2sK/gPsRm/2JwF7I7OAniIm0Y4LnlUNMIYORWcrFwNMEJw8IaosRc+6wBGUEuRZfBPQ/GRiZcN+1xnn4r9E8ZP1caUxuwn5vdnYqUTQ2xy5MvbdTiZSi5IA1kYTItyJlaypVjt8haw+3IrOivZEKFQPItpkkB6yAZBc5EynRY1YgMNt9JOOxuQe2mSmPpM1Tk42fgdihBmc7lUhxyV7Y7801RffIBitiD96zVBdUaSWHOO9chlSMKFdBTkKCjq9Fgou3QByCsmpqroQccl73EJw0PY/MoONKstwMXBTQxxLgd2jmmyAexF4P6uRUIsUVA7H9K/5N9hOBdEDWX71yP0Nt5M9tCJqBnyEjsKCg4bA2E3gCmYltjTtHGZf0AU4huK7fEiRbTTWKrQd2Caw84um7RRXHrWe2xb5e2zuVSHFFO2QJyPsszELCOrLOH/HLPR5ZKlMc0owouxuQj3ApJbkEWYj+ExKWMZj6mkVWS1fgNoKv3SNUZo5eEztIO4+sLWetsG5WaId4PXuvl8a4NS6XYL8/ezqVKBoH4Zd5Ln6nJiVluiOONxMorSy/RMIufo4kGlBKsx/BDlN/pryBxk7YKeLyiNOSmmrCOR7/9ZqOrv82KttgZ9iphcoh62Ovz+/uVKIGZhCSbGAOxZXl24gTxdrojLJSBiNB9ua1PT3Cvk3YeSjziFOQhlEUpy/2WtavnUqkuKIvtiXtPZL1zI+DPtjLZRc6lahBGQI8jD3yKrSFyILzkUgeVyUeVgKmYl/vYm7k3bCdWvLI+unaCcpaL9yM/7o9jg7+GpEm4EnsgecQl0JFoAUpnm0+w2phSpFeiGerGf9TaK8iqaca0aEnLTbAToc3i2DT4eoEz1LHhmyv+NkA/+BwDv7EB1mmL2KlyLqXZ61wIvZ7dJBTiaJxGX6Zx6HFGVKjHZLm7lvsh+d7ZM1SF5nT41fY98GMH9uO4Or015LtGNes0AK8hf/aHeVUotLkgE2RhB9LEJlfQLLbKJUzAjtE7B6yb3kwU0PORpwDlRQYgHi+mh/gKciIS19KN1yDbR5fDXmZT6Xtw1loC4ADnUham5yA//q9QnZjW9sjJaCCCo3n0RCjaugGfIb/ek5AHCKzzFBsh8KdnErUQKyHvRi+EHHH7upQLkVMraaH7MPISNj8cE4FNnIjZk2yCv5sSgvI5lpWX8Tpq1jo10dIHmKlMu7Afz0XAz91KlFpumOHnJ3jVKIGYhT2iOUZ2oq2Ku45l/APZqG9jlR2UKKRw3YCOdOpRDY/AW7BDi/wti+QJN7qb1A5+2Nf1yhe7C5pQgbQ5oA6q9aSuuJX+B2AfgB2I/s2/UZjGSTTUtjH82YkxZYSHTP/6H/IhoNNM2JqG0vxgdKLwC6o12S1DMIOs3uO7CugM7AtDjp4SoF++J1KlgA7OpVIKYaZQiuPlDw7Ch3olEsv/MmvFyOOIS7phhRNLpbjeSFiUlzPkYz1Rntsx7DpSGGGLLM9fi/w7xHPaiVmgqp8n47fXfl64LF0xFHK5DjgsIDfdwceTVmWeuBy/DVfL0UcblywKuLJfhDhM4aZSLrKG5B1bCU6TUjJv67I4MTbdsUeML2NpAUNIl/G73lEuXnbktb/LkQyq41HqiUtiXYqgDwvd9M2WM4D+wCflnEMJSJBitNMXP1SGoIoZdGEVKI/MeTvc1KUpV7YAvgfz78/BC5IWYYcUv3meKSmY5jF4CPEq/qvSExvo5BDBhG9kMG9qfCClGBY60J5FpntWltaLEISyV+HrFEuLrJtZyTJSU/Pb2cga/VKCuSwHYJ2cCqRYtIR+BvF17mOdCZdbdIRvxfiYiS3Z5r9/wZ4l+L39Skkx3Otm+BbEK/wwYiH6igkH/OxiMPbdcig4ElEeYxDTKVhiVfqvU0gPJQk13qtvNs/RO0/IzXHe/hvwruoo0FW6IVYAMwX61nj39e5ErBGOR//9bs4pX6HITPHYg5e8xBzbBYD15uQWc7qSLjTL4EDkBjYi5D193uR5/MtxNP3e9wrolpsi4B1Au7BMcZ2E9DMQIkTNCo5Fsku4+VSJF6smLlASZYByIzD/ICejlRJmeH57TmkfqRSmmHI4LCQTekjxMlmQUL9dUG81g8BNi6y3dfAGOBGRLEmTQuiBJdF4j57R/j/XtTWoHopEvsc1DZAilN7GYOY7BdGaHnkGrbzNPPfQb8V/t0eKeu3MvKurxUg/0FImcECmwLP07bktgjYBCmorSRIkOJsB7yDnULvNSSu6YukhVIs1kXMVst7fluE5AUuOATMoq1M22TkBVSK0wS8DIxs/fdSRJm9mUBf6yHKch+Khwe8DVwN3Id8kCulCamKsWJrW4HiijDLs5TFyPMdpvSCWtD28xAFZzIMue7e0K3HEfNo0PZJ0oQMlg42fv8Wead/aP33CojM3m/Ccai1ySkrAR9jmwvmIB5nSnr8HDue7HtgS2O79z1/n4EShSPwX9dLE+hjC+ANipvhlgAPIDOIKGtTHZH0ipshJc5ORpTt35EB7pfYuVVdt5nAJ8hA5SHgJuR6n46ETu2POERtiXi0DkJmYJ0iXpNKaY+9tjwNd0XdB2Jfu2/wp01sQa6jd5sH0HXNTNCH4Bd+PuL6rCTPaGRm6b3+k5ARsslj+D9SSnFWw+8I9zHx1lVcFfmYFVMms4Hf02YizCGzv7URh5lDgPOQRBZPIckYiq2HptUWA/9FBmvPIwr7D4hjz1GIKXpLxGq1HMHe+1nhUuzzcxm33hf/c/kQdvzoxfjl/YJsWwwajhYk5MH0tB3gUqgGIEdw4en3CA/Cvt6znSrO4rQgMzPvjC+u/KNdkFJOCyiufL5G0ubdi8wevqB4Cr0k2wKkaMO7iCPPXYjT0hnAoYiVaVMk3WYvsp89JyqbYdcUvsmpRMIWwEnIEo3Jdtj3Lk0PcKUM+gO3I2s/RziWpd5pQZx9zI/bMxRfGzvJs+23CctY6/wO/7W9IqbjdsM2oblocxHvyjeBJ4C/tJ7jqUiFnB0QL9hVW2VuRBNfd6SYu/e6fUa2i1esgJiRvTIf7VQiRckAXZEPnfkhvJXSNTSP9myvijOc9fCbv8ch62hxUCq+tpr2DRLS8QgysLoYCfvYD5mFrIcMcOM6l3rndvzXN+tVT5oRs7hX5vtpzEGPovw/y2Hnx8wj5YCivBznefaZlJCMtU4nJNykcJ2W0uZRWy3LU3mA/hwk7OFpxFR4FpIQYSskRjLOtVcF9sC+B+e6FCgC3vc7D3xO9muCKkqirIGscXlfjEWUV3j6Bs++miYxGLP491UxHrsnxdcoP0VChy4GDkeSBayFOHXorCE9VsB2sHqD0hYdl2yNfy12Ae6LDyiKU0Ziv8hzkDCUcrjfs/8dcQpYJ2yDrcg6x9zHz5AYvBcQT9NLkRJffYvso6RHE+Ir4H0OfkBCX7LK8ogHs1fmo5xKpCiO2Q0J7/G+FFMJTq1Vihc9xzg/LgHrhJ6I16jXRLupU4kUF5jp6fKI13BWaUaygHnl/TtqoVAamGOwXeE/pPKMP96EFQfFIWAdcTf+63y1W3EUBwzFHqQ+RraV0Jn45R2PrmsqDUqhJJg58n0diZGrFK+518wq1Mj8Gv91/oz4TbRKtmmPpKfzPgcuswNFYXMkvti7rqmFypWGpAXbDT6PmGOqiR9bxXOsJbTlrG10VgK+wx9ysJFTiRQXXIT9zrnMDlSKPsBX+OXV+HmlIWmHrE+YL/DDVB9ucKDneO9Weax6oQl7fehspxIpLtgE/8wtjzhwZZUmJCzJK+/fyLZJWVESoQOiIE2leQfx5PG803PMMTEcrx44Dv+1fpVs50xV4mcZ7DCvrGcHOg1b3mIZwxSlLumEJOg2leYY4sn5mcNv1tk7hmPWOkPxx1TORgsTNCK34n/nsm6q3xR/Eo0fCc5Xqyh1TVdgLLbSvJD4TC9rGMfuH9Nxa5X2SD1Z7zX5jVOJFBfshv3eneNUouL0RuroeuU93KlEiuKA7sAr2C/vKTH3460p+WHMx65FTEeQJOPemoEh1E+1kHqhH1KT1vsc/Ivsmupz+MsC5pGqObquqTQUvZDKFN4XYSnJBFs/6Onj1ASOX0uYjiBTqC7Epxhb05ZU4RM0vi4r5IAn8b97Wc8O5K1slEeyWum6ptJQ9EHqZnpfhEUks/a4LLCQtjCUFRPoo1bohiS+9g5UtkqgnxzyoTM9NbdNoK9yaELM9j0dy+GaI7GtPFnODvRT/NV6fqSyzGGKUrP0Q8yl3pd2PpLQOwlO8PTzTEJ91ApmDdO4amx66YydhSgPfI+7GWczkry+kACjkQPl1wDm4b83j5Jdk2d3pG5qrSh5RYmd/ojruPclmINkAEmCHP4SWfsm1E8tsBP+6/4uEgIUJwOxLQmFdkjMfUWlHbIW5pVlLo3pQdwO+Df+a/EN2U6wfwd+ee8mu0peUWJnIPbIcSawQYJ9jsSvoBs1jVxfJH2ad4Y/NOY+tsWuYFNoL+LGOSgoNng2UtS6EbkA+97s4FSi4uyFX9ZxaMYvpYEYjL/yRh6pcDIs4X69MWq3JtxXVskBj+C/9kfHfPxTsNczC20BYh5Mm87Y5bG+ROp7NiIjse/Rn5xKVJz++FNBLkTXNZUGYhh2rbzJwOoJ97sM4ilY6HOLhPvLKv+D/9o/SXymri7YZlCznRVTX+XQETs2+E2kbmMjYjqFFbxSu7gUqghN2PfvNKcSKUqKrANMJ32lCXCyp8+JNGYc4WrIel7hOkwnPuWxKvAfiivND5BkC2nSDDxgyPE22U4hlzQ3478ei4ENnUpUHO+7m0divZudSqQoKbEhflNLmkqzG36FfUEKfWaNFiT3rPf6x1XtYjvgW4orzaXAxjH1F5UcYn40n7kVUpYjS+yCfW/OcSpRcdahLXys4Mi1mlOJFCUlNkWcMFwoTYDfGX0PTqnfLGFmB4pjPSsH/BZ7rWwx9sfZRSL9cw0Z5gBrO5AjKyyPbfHJcnagTtihaq68sRUlVbbGv7aYttLsgX+me1dK/WaJUdjeiNWuZ3UluOTbf7Hrp04h/awu3rSKeUS5JxUbXAvkgCfwX5O5pPceVsJ1+OV9DA09URqAX+CvuJG20gQ4z9P3QhovXm9l/GEhC4ERVR5zdeB9bKX5OrA+ktzA+/vOVfZXLnsipmGvDCekLEPWOBz7fmV59rY9flln0LjOXEoDsSP+tYmC0kxzfaI3fhPxtSn2nQXaA2/gvwfHVnnM7bHXqvNIFqIOwP3G7/dX2V+5bIWEvHhluI3GnqkMxrb6PEJ2r8mywNf45d3NqUSKkgLbYH+80laaAFd6+p+N5MRtJExT14NU/rHMAadjz+QW0pbybGfjb9+TriPOuthr6f9CwlEalXbYg6csZwfKAQ/hl/d2pxIpSgpsSvCaZtpKcy38Tipnpty/a/bEfw8mUHky827YM8k8MisY2brNMthJLQ6rsL9KucXofxKN7UELtoNUnmyv9Zpxxl+iVXSUOmcEMAv3SrMJf+jF12Q3uDsJBuOfeS0CNqrwWIOwPRvzwGv4ldIY4+8vkX6s7HHAF4jz07WIY1gjsxG2h3OWswMNwh9nvJTGTVSiNAjDsXOTulCaAAcbcqQ983FJZ+xEBCdWeKxR2I4+hY+vNyH8xvhNuAuANSvsU4mHrtgFFLKcHSjIpHyVU4kUJWEGYS/mu1KaffAH448ju3FqSeDNx5unshJROeAMgtczTU/M9khGIO9251QouxIfN+K/J1nPDuT1fs8jz1Qjr00rdc7KyDpEFpQmwF8MWRrJG+9A7PWhXmUeoxt2iro8koQ/KPPPmcZ2HxF/eTKlPHbEvn9nV3nM3khIzx1IKbJvgf8FjqnyuCDPlTeJxiLE0UtR6pJ+2OagKbhTmlsbsjxOdl3u4+YnSHkw78en3BR3g/HXKy20V5B7bbIGtvf0JhXIrsRHX8Rr1ntPXqdyq8sg4AbsYtfemWw1A6WghPMXVXE8Rck0vbFNdNOBIY7k6YvfXDwNWM6RLGmzDLJ+5b0Xp5R5jB2wHbvyyEczKDF7E1JX07vtHyuQXYmPHGKa996TSrID5ZAB0EPY5nqzfVilzObSwjjURKvUKd2Bt/A/8N/jzrzShF1rMa4E5lknB9yH/9yfILpHaxNS6sv8QC5AQgPCOMTY/is0bMA15j3JI45y5dAHSW1XTFl626+rkHe3gONtVsXxFCWzdAFexv+w/4BbE52ZxP1Gh7KkzbHYpvJlI+67DPAw9sdrCsXDV/phe9vuWoHsSnysjj+UI0/52YG2RNayzefhYcTca/7+dJnH99ITuy5vlkNlFKViOgDPYs9MtnUo08/wOxZk2eU+bjbCn9ZwMZKAIgprAB9jfwxfpnROUDO5+0PlCl4lKyCDpT+Tfh7cLNKMxNV670k52YFagAuxrQ6fIvmm98Z+TsaVcfwg/mwcbypqsVDqkHbYs5PFSH0/VyyLP1tN1l3u46Q3khnHez9Oi7jvTgSvZ46hdKFp02PzB8SzOg3WAa7An9xhekp9R2UVJBxnLFL3NY1B3G+x72XU7EA9kWQV5v7nIwPlIdi1VidT3T3fPKA/l98RRUmEZuBu7Id9P4cyNSFes155qnW5rxWagCfxn/tTlF7XbEI+6uZ9/BEJZSnFMshHsxJlXQ09ESelIEeVF2Pqowsy6KrEQ7QZSRbxeICMo2OSL4zh2J7NUZcqlsdOlrEIOKD17+siTnbev0+jOgfAjshs1XvM+6o4nqJkkhy2WSWP1Dt0ycn45anG5b7WMNd0v6J0AvvuyJqXeR+nABtE7NdMqzeO0jPUasghH3Hz4+0976AwmXI5A1EYeWQmHrV8VTskjnFCiHx5ZB0wKdoB7xj9TUBCPEoxEBhv7DsXqX4DwVmj3kFm1JXSCZmFe4/5HVouTKkzcsDV2B+DU10KBfyUtg9d4YV3FTuaNlviX9NdQmlPxCHAJ9j38UWih+yY9RHzwM/LlL0chhNsQvTOkqMq/DBySDpC89gDI+w7iuBrarZnq5SxGOcG9LdFhP2GIoMOcya5AbASwQn9b0MUX7kMQMKUgtbT88BBFRxTUTLN+dgPuuvg5J7ARPwyFQubqCf6YXsilqr6sgt2qa08UnKsXcR++wT0m5R5rSuyjukdGAW1farspzt2GE8eeI7inqJDEbN4kExvIonmvb/9rUo5w9gKO4H7NRH264dtbv8c2B+4Cdszdxpixi/Xe7Yj8myGJU7IA/+s4LiKkmmCHA6ux+2DnsN2UHrIsUxp0YKdcKBYvGYTwQOfH4HflNFvUFD9D0D/ss+gdD+7Y3/Ug1q1g7e1sDNeFWbvw0P2aUYUgams8ohJ84jWbcwlhLhDLNoDl+C3OuQRs3nnEvt2ROqTmvKbxRnyyMDlSirzdN0ROxuQ2eZTfmIGRck0R2E/6H8h/TJRJmbM4tdEj1msdS7Bf+5fEJ6HtgfBQeyTgPXL7DcoqD5uh6DVCZ/Fme1hqnsO98auF1toY0L2WR6ZiQbtcwd+c7eZ5zdOh7XhwLsBMiwE1iuxb65V1lLXdwHwVyT9YrkMQgZz5jFfQwZs3t/KzWylKJnmAOwH/37cO95sgT9mMY/EmDUCO2CP1tcJ2XYodvq9PPAC5cfdrYQdtvIJ8TkEdUS8fM2Palj7EvHsrYQWZAYVduw3CZ6xbYud+zXf+ts2AdubxbwPCNimXJqQtdiw63RShGOcErJvoU1EBkSVxGZ2Ay7G9u6dgXgVH2b8/g7uvyeKEhvbYZuinsZ9tYvB2LFk5zuVKD1WwT73sI/xrsAc7I/iNURfzyyQI3jWGleyi+2xvTq9yjlo5hKkqKKwKv7C5kFKw/TsbEFMwkEhMPPdoYcAABF6SURBVC/jL+JdoFfAtptXKHOBAcDzRWR/ltIz8KHYg8488B6ynrwNYmYul3aIdSrI6/kWJNa4I/7BxBKk4L2i1AXrYn90X6X0uknSLIu9HvUg7s3GadAJOyfwDQHbNWO7+RdmpvtX2PevA44Xh0PQGoTnQp2JhHd0QRIbeP9WSQL5HOLYEjSYKLRZwDBjv/5IRZig7a8kfBDys4DtB1Qgd0H2AwhOVFFo0ykdjtOE7Z38KtUVQMgheWaDLBsfItehwAnG36+vol9FyRQrY+eo/IDyaznGTQfsl/49xPOy3skha03ec38d20zag+DZ2ZeUXvcKow+24lpEdSE/PYHfE+wtuxC4qnUbgL2Mv8+ndJxq0DkE1RU1z8mcxe5IsLPMbMR5qRimSXIxlZkklyU4JMRsUQoZHGTss5TqZnybYKf3yyPes6fhfz674X+O/os8r4pS8/TALg82CVnfckkOuBO/XN9Q+Qi+1jDjC6dh35NhBHuHjqV8ReMlyIkkaKYbhRbE43RGwDEL1oNBxj5jjW3KmW3mkFl2WH/e5o0hbI8o9qDt3ieas8x1xn4Ty5AbZHa4D/7yeGEtzJHJSx/sQcBtZcpUYA3kXgXJcjfBiRHOMrbbt8K+FSVTdMBeP5kJrOlSqFbMl24hjVMkeVvsJAdbGtvsjh1zl0c+/tU4XqyJva43j+A1vVJsgyidoI/tOwQH6w82tltKdA/Pgdjl5WYQPLi4wNjvzRA57yB6vlkzbOeFiPuBrIX+O6D/GcB5+IuUfxVRpouNY82l/Pu4HDJoCgrDeZHwJBTd8JuZXyI7YWNNiHVjIGKV2RrYAynBdgQSrrU7kuBjJFIkflXEcaoT2TkPxQFBpsB5wMYuhWolaH1ttEuBUmQ1bGegkz1/b8b+IBbuXRwjenOWnwcuK/MYg7CVSKFNRe5lmDOKeW6fRuivF5KG0BtmsgjJerUrdszjX2n7+O2EpH0z5VyAmF7L+UiaM+V/RNhnDYJLu+WB25FZozkYiJLwoxP2rPt30U+FrkgoTdD68CfIdSt2bUYb+2xdRt/l0Iw4dq2HeJ8fipznFcDNiMn7n8hA7QvkXpcqzl2qLUEGBV8h8bNvIYOkx4F7kRSlVyODs98iDlQHIOvCBWW8FqK4+6DKuKYwP1CLkQfPNSOx3e6vcipRenTFnqHdR9tL1ZPgeMeJxFNEfHlsJTOL6GvdPZB7FeTBOQ+ZOZVanzaVyBtFth2GJBgws9M8gaQZXA7b7Pk8YmlpB1weIGce+cBWks7PnLV+VWTbvsAfCJ7JfUabotnf+NuHRLMoHGjsN4NoKfM6IgOGIHPxNOBIonlov4D/HparGHLIWu9PEA/sg5AEFDcgz8ibiLdu0PWrxbYYSaQxBRmYFJTxY8A9tCnj85GUp0ciz8YuSBap9RHLTD/EGqGKOAFMJ4Y82ZjRrYrtlPIUlbnK1xpN2M4sH9OWsHs4weEbzxFfEojRAcc/K8J+Q4BrsZOCF9qdRM809Lax7wvIhzqHrG/viozkXzC2W4rMLgoKrxmZaXi3mYwMPlbELsZeaHdQeazoRwHH82bH6YvMPO4heCY3BXk323u2N9cod4ogRw47UcIrJfZZHhnYBIWWzEdCc6Jel4HG/mE1U/sgFWn2Bk5HUv49h2QdMmNCtZXXliCz60mID8trSGjhfUio0NXI/T4ZmaXvjQxQhuA+/DCT/BJ7VpFGaahS9MROBP0xjVPc9kz85z6HtrXmPQlez7yCeAPJ7zWOv4hwJ6MWZB3IVE7e9gzle/YGJRvIE57tZwHywTXXQc8L2PaXyLprkHKYRfX5b18POO50xHkmaP2y0KYBxyOzPS/mUsprRJtJdA/o472QfUcgg4UgK8FSxFxcbnrFsz3H+IC20LHOSHL86wmP4U2rLUQGJVMRC8NHiDn3dWRQ9goy6/sAsQBMRu7lHOpnlhvWliBVdv6BOOadhAx+hmI/o5Gp5Snw+siCvjc281raYq1c0QGpL7mV57fvkNHoeCcSpcuOyJqglz2RPLwXYg9s5iPrXPfELMdY/E5IT9JWELkDsh43HDEL70O4o8lbiMz/rECG+xBHjVLMRl7qaxGzopctsROI34OYwM7FfodfQ2rLTihfXB+3Eq2uaYHvEHPxGGRg5GUdZNboZUuiORx1Q66PyRhkHW4Q4sswkvAyYWOR2YgpQylyiKIphC6dh1gifoE4QMU5m1mEXMOw9m2RvxXM+5WQQ6wgnT2tSwL/zqKlLY9YRt5Alj3G0lZftS4ZiD2avwf3iQRasGPWFpOcM0HWWBO7esnlyLqi6RSSRz7uaycki+nQ8yXwd2Q0HmWU/Rai9KoZXA4kPLXcVGQwcTzFLRFmlqDF2Cbgwsj6XOKbtY+gdFWXPDJrOY/iMY3mWu8zZcjRNYIMYe0lZLBU6T0cUEXf3jYfme09jFhWDkO+CWshYVn1vo6XQ0z2PZAB6iDEerMNEud8BGKluhqxCjyOzJbHIevZplUxyTYVsY7sQPkZyjJNL+zagf/AvS27CRmlmzfiKJdCpUgP7OwrzyMzuqDqEs8iacyS4siAPqO0ZxBrQVwfsj6IAh6DePTuRvS44k0iyjwR2DQmeb3siawrmf3NRky2e1DaQWpEwP7lJC3IIe931Pv3DTJYW6OMPsLYtIx+84ip/UVEOR6KzKr7435AX+sUQm5WBzZCrFpHIGvVdyDfmfFEzxMdtf0XcRBcK/lTTJaO2I4QbxGtSnyShBXJvor6HkkWaEZMod5zn4KknAtaz7uc5BNjNyEOBFFekIWIIghLNu+KGykt+z0km8GmA3JddkUUwSqUd+/MlISVpDrsiYRjBK3nLkHMqfe3yhjnLGHDgP7M5+YVxMFra9yn9Gx0cogT2nrIOubRyGD1bsT6MIFoVpSgNpYaLR3XhBTS9Z7MeCqrgBA3XgeCQruZxlCaYIcDLcGut5lHlOheKcrVgnh+FtYtCm0Wouh/B2xGtNAGF4QlXMgjs8x9yfYztj72czGkymP2Qe7ZnoiZP8l714TfLL4QWUO+GEnsETWZhJIdWpAllG2AwxHrwEOIKb2USXge4lyU5XfO4grsaXQ1+Ubj4jjsC/x3srkYngS/ItqI7XMkjs0FOST8YDASL1kL9yaHxDma1/ELxJmqFtZfzHSHt7oVpyKakETvQ6iNa65UTjdEoZ4L/Ifwb1klcdFOOBq/4LOJJ0i+WkZjX9Snia/GY9ZZm/DQCm97BvdJ9muRfsBdSBKEc5BSebXy8e6JP7XeIhonN7NS++QQb+2gAgVnOJQrMjvjn0IvIhteqrthT+1foXHMN8si5sJSSvMSamOGp8TLMfifg7vdiqMoZTEEKXRgRgnMRZKOZJqNsFOQHVR0j3TYFjsbyLs0TpmhFuxcpmabi6xDKY1HDnt9diOnEilKcZoQJ7hTsTN5eS2duzqSLzKrYaesu9SpRMLG2ObJcWTDSSktrqG40hyPJBdQGpON8T8PxXL0KoorVkbC1u4lPMtXob1ODXjV9saOCXwA9/FQa2NXn5iE3IBG4TcUf8Ceoq2Is9KY3Ib/mdDalUrW2IHgHMtmexwJw8q8J2177FCGf+M+RmoQ9qhkGtHrK9YDG1I8yPgidD2z0emBf3llDlXkBFWUhDBzJ3vbfKRSUbWhU6mRw86+MxnxLnRJfyRlm1eu78mGZ29aLI+Ulgpbz9zdnWhKhjAzNj3gVhxFCcR0XluIZDM7hvBiEJnlt/hPZg7J5TKNSl/sFH/zkHRojUIH7JyphfYZEhupKCDB5N7nY7RTaRQlmCZk2elSJBa9ZitX7Yb/hVuC+2LUPZHyPF65FiDxdI1CDil1FaQ0n6BxPImV0uSQxCSF52MpNTh6V5RaYQR22MlxTiWSmeZ72LbvRlKaIFlqgpTmBbh31lKyxarYnoiKoiTAithrZzfg1otpBexC1PPIRuKFNCnUIzTN55mPZVKcsBf+Z+V2t+IoSn3SBbu24DMkXzmjGAOwK7rPRRJLNxrN+EuCjUMqpytKEAfgf28u8/ytN5Kq7C5qMFG2omSFJuBB/C/ah7hdpB2EXX9wFlJdvlHZAClddRq6nqkUx5xxTkIyfd2InTRkP0cyKkpNcwn+F+kbpNafK4YBXxsyfYfELiqKUppdKB1QXmgXOpJRUWqW0fhfoh+RNF2uWBeYYcg0EymIqihKNJZD4uFKKc3/UMPu/4rigs2wX640ixub/BRJZuCVZxruakcqSi1zIuEK811gb9z6MChKzbEaMpPzvkxnOZRnc+x8hV+jDjCKUg2bAGOQ5Nm3A4cgqSnVIUhRyqQLdqmh+3H3Mm2Hv9BuHphCY+WeVRRFUTJKDhl9epXUeNytc+yMXU/zS2RGrCiKoijOOQk7bZ0rx5u9gcWGPJ/j1qNXURRFUf6frZC8s15FdYQjWQ5E8md6ZXkPqfyhKIqiKM5ZGZiOX1Hdi5t1zaMMOfLAC6hbvKIoipIROgJv4VdU44BuKcuRA87DVpoPogV2FUVRlIwQVJB6PunHRrYLkCOPlMlqTlkWRVEURQnlCGxldXDKMnQDng6Q4wI0nkxRFEXJECOxMwPdSbrKqh92AeqlwDEpyqAoiqIoJekHTMWvsD4GuqYowxBgoiHDQiQMRVEURVEyQ3vgFfwKax4wPEUZNsFO6TcX2DZFGRRFURQlEmOw1xNHp9j/7kiVFW//04H1U5RBURRFUSKxP7bSvC3F/o/FTmwwEc07qyiKomSQ/sAs/ErrA6BzCn03AVdgK+33gRVS6F9RFEVRyiKHHfIxF1gzhb47APdgK82XgZ4p9K8oiqIoZXMwtuLaN4V+eyDp8sy+HwU6pdC/oiiKopTNysBs/IrrlhT67Y+Ygk2leStaWV5RFEXJKDngWfyKaxKwTML9rg18ha00L0WzASmKoigZ5hBs5bVdwn3uhKyfmv2ekHC/iqIoilIVA7BNtDcn2F8OOBU73GQRsF+C/SqKoihK1eSAf+BXYFNIrqZlByQe1Jxl/gD8IqE+FUVRFCU2DsNWYqMS6qsPElpi9jcT+GlCfSqKoihKbKwCzCGd7EDDgQnYSnMy6cSIKoqiKEpV5IDn8CuxqSSTaGAU9hpqHvgQCUVRFEVRlMxzOLYi2zHmPnLA8cCSgL6eIPlQF0VRFEWJhVWwTbT3x9xHe+AmbIWZB64EmmPuT1EURVES43HsGpsrx3j83sDz2ApzIXBQjP0oiqIoSuJsjq3Qzozx+EOAzwL6mA5sGmM/iqIoipI4OeBf+BXa50DHmI6/LfA9ttJ8HzEPK4qiKEpNsRvJOQQdBSwOOP6jQLeY+lAURVGU1GgBxmF7tlabSL0ZuA5bYRYStasTkKIoilKTHIpfqS0ABlV5zK7AY9gKcwFwQJXHVhRFURRndEaSG3iV28VVHnNF4F1spfkNMLLKYyuKoiiKU07Cr9y+QmaLlbIOkgjeVJrvIpVWFEVRFKVmaQEm4ldwx1ZxvB0IrqF5A/F55yqKoiiKM0xP2hlAlwqPdQx2+rzZwF7Vi6koiqIo2eAF/Iru7AqO0QxcS7BptloHI0VRFEXJDOvgV3RzkXR45RDmOaumWUVRFKXuuBW/svt9mfuvCLxjHGMOsHeMMiqKoihKJugD/Ig/wfpKZey/Ebbn7HuoaVZRFEWpU07Dr/T+GnG/jsDl2E5Af0RNs4qiKEod8yZ+xTcqwj4bAx+jpllFURSlweiHX/l9ixSWDqMTUlx6KWqaVRRFURqQUfgV4C1Fth2Jnfx9KWKuVdOsoiiK0hAchF8R7mH8vQnJAPQPY7tCfU4tOK0oiqI0DC3YsZo5YAXEU3YjJJtQkAn2j8CpSLynoiiKojQMm2ObXoPqZRbaFODnTiRVFEVRlAzQDviO4soyj4Sc/AXo6URKRVEURckQm2PX4Cy074ArgIHOpFMURVGUjJDz/H9fJKn7WkhFlM+QpO9/BX5IXTJFURRFySD/B23ghNNK1rDHAAAAAElFTkSuQmCC"; + +export const AnalyticsTemplate = ({ + greeting, + domain, + periods, +}: AnalyticsTemplateProps) => { + const preview = `Ihr Website-Report für ${domain}`; + + return ( + + + + +
+ Mintel + {domain} +
+ +
+ {greeting} + + hier ist Ihr aktueller Website-Report. + +
+ + {periods.map((period, i) => ( +
+ {period.label} + + + + + + + + + + + + + + +
+
+ Besucher + {period.visitors.toLocaleString("de-DE")} + Wie viele verschiedene Personen Ihre Website besucht haben. +
+
+ +
+ Besuche + {period.visits.toLocaleString("de-DE")} + Wie oft Ihre Website insgesamt aufgerufen wurde. +
+
+ +
+ Seitenaufrufe + {period.views.toLocaleString("de-DE")} + Wie viele einzelne Unterseiten angeschaut wurden. +
+
+
+ Absprungrate + {period.bounceRate}% + Anteil der Besucher, die nach nur einer Seite sofort wieder gehen. +
+
+ +
+ Verweildauer + {period.visitDuration} + Wie lange Besucher im Schnitt auf der Website bleiben. +
+
+ +
+ + {period.chartData && period.chartData.length > 0 && ( +
+ Seitenaufrufe + + + + {period.chartData.map((d, i) => { + const maxValue = Math.max(...period.chartData.map(cd => cd.value), 1); + const h = Math.round((d.value / maxValue) * 100); + return ( + + ); + })} + + +
+
+
+
+ )} + + + + + + + + +
+
+ Seiten + Die beliebtesten Unterseiten sortiert nach Aufrufen. + {period.topPages.map((p, idx) => ( + + {p.url} + {p.views.toLocaleString("de-DE")} + + ))} +
+
+ +
+ Quellen + Woher Ihre Besucher auf die Website kommen. + {period.topReferrers.map((r, idx) => ( + + {r.url || "Direktaufruf"} + {r.visitors.toLocaleString("de-DE")} + + ))} +
+
+
+
+ ))} + +
+ + + ); +}; + +export default AnalyticsTemplate; + +const main = { + backgroundColor: "#fafafa", + color: "#111111", + fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif', + WebkitFontSmoothing: "antialiased" as const, +}; + +const container = { + margin: "0 auto", + padding: "40px 20px 80px", + maxWidth: "600px", +}; + +const headerSection = { + marginBottom: "32px", +}; + +const logoImg = { + marginBottom: "16px", +}; + +const domainText = { + fontSize: "24px", + fontWeight: "bold", + margin: "0", + letterSpacing: "-0.02em", +}; + +const introSection = { + marginBottom: "32px", +}; + +const introText = { + fontSize: "15px", + lineHeight: "24px", + color: "#444444", + margin: "0", +}; + +const periodSection = { + marginBottom: "40px", +}; + +const periodLabel = { + fontSize: "14px", + fontWeight: "bold", + textTransform: "uppercase" as const, + color: "#111111", + marginBottom: "16px", + letterSpacing: "0.05em", +}; + +const cardsTable = { + marginBottom: "24px", +}; + +const cardTd = { + paddingBottom: "16px", +}; + +const card = { + backgroundColor: "#ffffff", + border: "1px solid #e5e7eb", + borderRadius: "8px", + padding: "16px", +}; + +const cardTitle = { + fontSize: "12px", + fontWeight: "bold", + color: "#111111", + margin: "0 0 8px 0", +}; + +const cardValue = { + fontSize: "24px", + fontWeight: "900", + color: "#111111", + margin: "0", + letterSpacing: "-0.02em", +}; + +const cardExplanation = { + fontSize: "11px", + lineHeight: "1.4", + color: "#6b7280", + margin: "8px 0 0 0", +}; + +const chartContainer = { + backgroundColor: "#ffffff", + border: "1px solid #e5e7eb", + borderRadius: "8px", + padding: "16px", + marginBottom: "24px", +}; + +const chartTitle = { + fontSize: "14px", + fontWeight: "bold", + color: "#111111", + margin: "0 0 16px 0", +}; + +const tablesContainer = { + marginBottom: "16px", +}; + +const listContainer = { + backgroundColor: "#ffffff", + border: "1px solid #e5e7eb", + borderRadius: "8px", + padding: "16px", +}; + +const listTitle = { + fontSize: "16px", + fontWeight: "bold", + color: "#111111", + margin: "0", +}; + +const listSubtitle = { + fontSize: "12px", + color: "#6b7280", + margin: "4px 0 16px 0", +}; + +const listRow = { + marginBottom: "12px", +}; + +const listText = { + fontSize: "13px", + color: "#444444", + margin: "0", + wordBreak: "break-all" as const, +}; + +const listTextBold = { + fontSize: "13px", + fontWeight: "bold", + color: "#111111", + margin: "0", +}; + +const divider = { + borderTop: "1px solid #e5e7eb", + borderBottom: "none", + borderLeft: "none", + borderRight: "none", + margin: "32px 0 0 0", +}; diff --git a/packages/mail/src/templates/MonthlyAnalyticsTemplate.tsx b/packages/mail/src/templates/MonthlyAnalyticsTemplate.tsx deleted file mode 100644 index 92153dc..0000000 --- a/packages/mail/src/templates/MonthlyAnalyticsTemplate.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import * as React from "react"; -import { Heading, Section, Text, Row, Column } from "@react-email/components"; -import { MintelLayout } from "../layouts/MintelLayout"; - -export interface MonthlyAnalyticsTemplateProps { - clientName: string; - domain: string; - month: string; - year: string; - totalVisitors: number; - totalPageviews: number; - topPages: { url: string; views: number }[]; - topReferrers: { url: string; visitors: number }[]; -} - -export const MonthlyAnalyticsTemplate = ({ - clientName, - domain, - month, - year, - totalVisitors, - totalPageviews, - topPages, - topReferrers, -}: MonthlyAnalyticsTemplateProps) => { - const preview = `Ihr Website-Report für ${month} ${year}: ${domain}`; - - return ( - - Monatsreport: {month} {year} - - Hallo {clientName},

- hier ist der monatliche Performance-Report für Ihre Website ({domain}). - Die Daten zeigen, wie viele Menschen Ihre Seite besucht haben und welche Inhalte am beliebtesten waren. -
- -
- - - BESUCHER - {totalVisitors.toLocaleString('de-DE')} - - - SEITENAUFRUFE - {totalPageviews.toLocaleString('de-DE')} - - -
- -
- Top Seiten - {topPages.map((page, i) => ( - - -
- - - {page.url} - - - {page.views.toLocaleString('de-DE')} Aufrufe - - - ))} -
- -
- Top Besucherquellen - {topReferrers.length > 0 ? topReferrers.map((ref, i) => ( - - -
- - - {ref.url || "Direktaufruf"} - - - {ref.visitors.toLocaleString('de-DE')} Besucher - - - )) : ( - Keine externen Quellen verzeichnet. - )} -
- - - Haben Sie Fragen zu diesen Zahlen oder möchten Sie die Reichweite Ihrer Seite weiter ausbauen? - Antworten Sie einfach auf diese E-Mail. - - - - Beste Grüße,
- Marc Mintel
- Digitaler Architekt -
-
- ); -}; - -export default MonthlyAnalyticsTemplate; - -const h1 = { - fontSize: "28px", - fontWeight: "900", - margin: "0 0 24px", - color: "#ffffff", - letterSpacing: "-0.04em", -}; - -const h2 = { - fontSize: "14px", - fontWeight: "900", - textTransform: "uppercase" as const, - color: "#444444", - margin: "0 0 16px", - letterSpacing: "0.1em", -}; - -const intro = { - fontSize: "16px", - lineHeight: "24px", - color: "#cccccc", - margin: "0 0 32px", -}; - -const metricsContainer = { - backgroundColor: "#151515", - padding: "32px", - borderRadius: "8px", - marginBottom: "32px", - border: "1px solid #222222", - textAlign: "center" as const, -}; - -const metricColumn = { - width: "50%", -}; - -const metricLabel = { - fontSize: "12px", - fontWeight: "bold", - color: "#888888", - letterSpacing: "0.1em", - margin: "0 0 8px", -}; - -const metricValue = { - fontSize: "36px", - fontWeight: "900", - color: "#4CAF50", - margin: "0", - letterSpacing: "-0.02em", -}; - -const auditContainer = { - backgroundColor: "#151515", - padding: "32px", - borderRadius: "8px", - marginBottom: "32px", - border: "1px solid #222222", -}; - -const highlightRow = { - marginBottom: "12px", -}; - -const bulletCol = { - width: "24px", - verticalAlign: "top" as const, -}; - -const bullet = { - width: "6px", - height: "6px", - backgroundColor: "#4CAF50", - marginTop: "8px", -}; - -const highlightText = { - fontSize: "15px", - color: "#ffffff", - margin: "0", - lineHeight: "22px", - wordBreak: "break-all" as const, -}; - -const highlightNumber = { - fontSize: "15px", - color: "#888888", - margin: "0", - whiteSpace: "nowrap" as const, -}; - -const bodyText = { - fontSize: "16px", - lineHeight: "24px", - color: "#888888", - margin: "0 0 32px", -}; - -const footerText = { - fontSize: "14px", - color: "#666666", - lineHeight: "20px", -}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ba25cc..840a033 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,7 +53,7 @@ importers: specifier: ^16.3.2 version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@types/node': - specifier: ^20.17.16 + specifier: ^20.19.33 version: 20.19.33 '@types/react': specifier: ^19.2.10 @@ -129,7 +129,7 @@ importers: specifier: workspace:* version: link:../../packages/tsconfig '@types/node': - specifier: ^22.10.2 + specifier: ^22.19.10 version: 22.19.10 tsup: specifier: ^8.3.5 @@ -140,6 +140,9 @@ importers: typescript: specifier: ^5.7.2 version: 5.9.3 + vitest: + specifier: ^4.1.10 + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) apps/sample-website: dependencies: @@ -272,7 +275,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) packages/content-engine: dependencies: @@ -574,7 +577,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) packages/meme-generator: dependencies: @@ -633,7 +636,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.1.3 - version: 2.1.9(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) + version: 2.1.9(@types/node@20.19.33)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) packages/next-config: dependencies: @@ -789,7 +792,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.0 - version: 2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) + version: 2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) packages/outline-mcp: dependencies: @@ -820,7 +823,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.1.3 - version: 2.1.9(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) + version: 2.1.9(@types/node@20.19.33)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) packages/page-audit: dependencies: @@ -965,7 +968,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) packages/serpbear-mcp: dependencies: @@ -1088,7 +1091,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.1.3 - version: 2.1.9(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) + version: 2.1.9(@types/node@20.19.33)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) packages: @@ -3827,6 +3830,9 @@ packages: '@vitest/expect@4.0.18': resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + '@vitest/mocker@2.1.9': resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} peerDependencies: @@ -3860,6 +3866,17 @@ packages: vite: optional: true + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/pretty-format@2.1.9': resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} @@ -3869,6 +3886,9 @@ packages: '@vitest/pretty-format@4.0.18': resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + '@vitest/runner@2.1.9': resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} @@ -3878,6 +3898,9 @@ packages: '@vitest/runner@4.0.18': resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + '@vitest/snapshot@2.1.9': resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} @@ -3887,6 +3910,9 @@ packages: '@vitest/snapshot@4.0.18': resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + '@vitest/spy@2.1.9': resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} @@ -3896,6 +3922,9 @@ packages: '@vitest/spy@4.0.18': resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + '@vitest/ui@4.0.18': resolution: {integrity: sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==} peerDependencies: @@ -3910,6 +3939,9 @@ packages: '@vitest/utils@4.0.18': resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + '@vladfrangu/async_event_emitter@2.4.7': resolution: {integrity: sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} @@ -7798,6 +7830,9 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -8084,6 +8119,10 @@ packages: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + tinyspy@3.0.2: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} @@ -8560,6 +8599,47 @@ packages: jsdom: optional: true + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vizion@2.2.1: resolution: {integrity: sha512-sfAcO2yeSU0CSPFI/DmZp3FsFE9T+8913nv1xWBOyzODv13fwkn6Vl7HqxGpkr9F608M+8SuFId3s+BlZqfXww==} engines: {node: '>=4.0'} @@ -11830,6 +11910,15 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.0 + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@20.19.33)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0))': dependencies: '@vitest/spy': 2.1.9 @@ -11870,6 +11959,14 @@ snapshots: optionalDependencies: vite: 7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + '@vitest/mocker@4.1.10(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + '@vitest/pretty-format@2.1.9': dependencies: tinyrainbow: 1.2.0 @@ -11882,6 +11979,10 @@ snapshots: dependencies: tinyrainbow: 3.0.3 + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.0 + '@vitest/runner@2.1.9': dependencies: '@vitest/utils': 2.1.9 @@ -11898,6 +11999,11 @@ snapshots: '@vitest/utils': 4.0.18 pathe: 2.0.3 + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + '@vitest/snapshot@2.1.9': dependencies: '@vitest/pretty-format': 2.1.9 @@ -11916,6 +12022,13 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + '@vitest/spy@2.1.9': dependencies: tinyspy: 3.0.2 @@ -11926,6 +12039,8 @@ snapshots: '@vitest/spy@4.0.18': {} + '@vitest/spy@4.1.10': {} + '@vitest/ui@4.0.18(vitest@4.0.18)': dependencies: '@vitest/utils': 4.0.18 @@ -11955,6 +12070,12 @@ snapshots: '@vitest/pretty-format': 4.0.18 tinyrainbow: 3.0.3 + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + '@vladfrangu/async_event_emitter@2.4.7': {} '@webassemblyjs/ast@1.14.1': @@ -16417,6 +16538,8 @@ snapshots: std-env@3.10.0: {} + std-env@4.2.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -16766,6 +16889,8 @@ snapshots: tinyrainbow@3.0.3: {} + tinyrainbow@3.1.0: {} + tinyspy@3.0.2: {} tinyspy@4.0.4: {} @@ -17227,7 +17352,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vitest@2.1.9(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0): + vitest@2.1.9(@types/node@20.19.33)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0): dependencies: '@vitest/expect': 2.1.9 '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.19.33)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)) @@ -17265,7 +17390,7 @@ snapshots: - supports-color - terser - vitest@2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0): + vitest@2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0): dependencies: '@vitest/expect': 2.1.9 '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)) @@ -17303,7 +17428,7 @@ snapshots: - supports-color - terser - vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 @@ -17348,7 +17473,7 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 @@ -17434,6 +17559,37 @@ snapshots: - tsx - yaml + vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 22.19.10 + '@vitest/ui': 4.0.18(vitest@4.0.18) + happy-dom: 20.5.3 + jsdom: 27.4.0(canvas@3.2.1) + transitivePeerDependencies: + - msw + vizion@2.2.1: dependencies: async: 2.6.4