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 */}
+
+
+

+
+
+
+ {/* Title Area */}
+
+
+ {projectName} GATEKEEPER
+
+
+
+
+
+ |
+
+
+ {subTitle}
+
+ |
+ |
+
+
+
+
+
+ {/* Main Content Area */}
+
+
+ {/* Footer Area */}
+
+
+
+

+
+
+ © {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,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";
+
+export const AnalyticsTemplate = ({
+ greeting,
+ domain,
+ periods,
+}: AnalyticsTemplateProps) => {
+ const preview = `Ihr Website-Report für ${domain}`;
+
+ return (
+
+
+
+
+
+
+ {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