Finalize metrics verification and backlog updates
This commit is contained in:
18
app/api/internal/rybbit/campaign/route.ts
Normal file
18
app/api/internal/rybbit/campaign/route.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { fetchRybbitCampaignAnalytics } from "@/lib/rybbit-analytics";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
const result = await fetchRybbitCampaignAnalytics({
|
||||
apiUrl: process.env.RYBBIT_API_URL,
|
||||
apiKey: process.env.RYBBIT_API_KEY,
|
||||
siteId: process.env.NEXT_PUBLIC_RYBBIT_SITE_ID,
|
||||
startDate: url.searchParams.get("startDate") ?? undefined,
|
||||
endDate: url.searchParams.get("endDate") ?? undefined,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
return Response.json({ ok: false, error: result.error, data: result.data });
|
||||
}
|
||||
|
||||
return Response.json({ ok: true, data: result.data });
|
||||
}
|
||||
@@ -4,7 +4,7 @@ title: Add follow-up and manual sales status tracking
|
||||
status: In Progress
|
||||
assignee: []
|
||||
created_date: '2026-06-03 19:14'
|
||||
updated_date: '2026-06-05 19:30'
|
||||
updated_date: '2026-06-05 19:49'
|
||||
labels:
|
||||
- mvp
|
||||
- sales
|
||||
@@ -25,11 +25,11 @@ Add the lightweight CRM layer needed after first contact. The MVP does not parse
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 After an initial send, a single follow-up draft and suggested due date are created
|
||||
- [ ] #2 Follow-up sending requires manual review and approval, just like the first email
|
||||
- [ ] #3 Manual statuses exist for Antwort erhalten, Kein Interesse, Später wieder melden, Gespräch vereinbart, Angebot angefragt, Angebot gesendet, Auftrag gewonnen, Auftrag verloren, Nicht weiter verfolgen, Follow-up geplant, and Follow-up gesendet
|
||||
- [ ] #4 Marking Antwort erhalten or Kein Interesse stops pending follow-up prompts
|
||||
- [ ] #5 Nicht erneut kontaktieren blocks outreach for 12 months and then reappears only as Erneut prüfen
|
||||
- [x] #1 After an initial send, a single follow-up draft and suggested due date are created
|
||||
- [x] #2 Follow-up sending requires manual review and approval, just like the first email
|
||||
- [x] #3 Manual statuses exist for Antwort erhalten, Kein Interesse, Später wieder melden, Gespräch vereinbart, Angebot angefragt, Angebot gesendet, Auftrag gewonnen, Auftrag verloren, Nicht weiter verfolgen, Follow-up geplant, and Follow-up gesendet
|
||||
- [x] #4 Marking Antwort erhalten or Kein Interesse stops pending follow-up prompts
|
||||
- [x] #5 Nicht erneut kontaktieren blocks outreach for 12 months and then reappears only as Erneut prüfen
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
@@ -46,4 +46,6 @@ Add the lightweight CRM layer needed after first contact. The MVP does not parse
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Started implementation pass for tasks 15-19 and 27. TASK-27 note says it is superseded by TASK-13, so this pass will verify the existing PageSpeed-to-audit-generation handoff rather than implement it separately.
|
||||
|
||||
Implemented and verified follow-up draft creation after send, manual approval boundaries for follow-up records, manual sales status labels/mutation, reply/no-interest suppression, and 12-month do-not-contact recheck visibility. Verification: pnpm test 305/305; pnpm lint 0 errors.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Orchestrate recurring Convex agent jobs and audit lifecycle
|
||||
status: In Progress
|
||||
assignee: []
|
||||
created_date: '2026-06-03 19:14'
|
||||
updated_date: '2026-06-05 19:30'
|
||||
updated_date: '2026-06-05 19:49'
|
||||
labels:
|
||||
- mvp
|
||||
- convex
|
||||
@@ -27,11 +27,11 @@ Implement the scheduled and manual background workflow using Convex. The MVP per
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 Convex cron or scheduled functions trigger active campaigns according to cadence
|
||||
- [ ] #2 Jetzt ausführen starts a campaign run immediately only when no other agent run is active
|
||||
- [ ] #3 Cron skips or queues safely when an agent run is already active, with visible run logs
|
||||
- [ ] #4 Published audits older than 30 days create dashboard notifications asking whether to keep active
|
||||
- [ ] #5 Published audits older than 60 days auto-deactivate unless manually extended or later reactivated
|
||||
- [x] #1 Convex cron or scheduled functions trigger active campaigns according to cadence
|
||||
- [x] #2 Jetzt ausführen starts a campaign run immediately only when no other agent run is active
|
||||
- [x] #3 Cron skips or queues safely when an agent run is already active, with visible run logs
|
||||
- [x] #4 Published audits older than 30 days create dashboard notifications asking whether to keep active
|
||||
- [x] #5 Published audits older than 60 days auto-deactivate unless manually extended or later reactivated
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
@@ -48,4 +48,6 @@ Implement the scheduled and manual background workflow using Convex. The MVP per
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Started implementation pass for recurring Convex agent jobs, run locking, logs, and audit lifecycle.
|
||||
|
||||
Implemented and verified Convex crons, due-campaign runner, single-active-run guard, visible campaign run logs, and audit lifecycle notification/deactivation controls. Verification: pnpm test 305/305; pnpm lint 0 errors.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Add Rybbit audit analytics dashboard
|
||||
status: In Progress
|
||||
assignee: []
|
||||
created_date: '2026-06-03 19:14'
|
||||
updated_date: '2026-06-05 19:30'
|
||||
updated_date: '2026-06-05 19:49'
|
||||
labels:
|
||||
- mvp
|
||||
- analytics
|
||||
@@ -25,11 +25,11 @@ Display anonymous analytics for generated public audit pages inside the internal
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 Rybbit tracking runs only on public audit pages, not on internal dashboard routes
|
||||
- [ ] #2 Dashboard can fetch Rybbit API data for pageviews, custom events, and outbound link clicks for audit pages
|
||||
- [ ] #3 Per-audit analytics show opened yes/no, view count, last view, CTA clicks, website-link clicks, and device type where available
|
||||
- [x] #1 Rybbit tracking runs only on public audit pages, not on internal dashboard routes
|
||||
- [x] #2 Dashboard can fetch Rybbit API data for pageviews, custom events, and outbound link clicks for audit pages
|
||||
- [x] #3 Per-audit analytics show opened yes/no, view count, last view, CTA clicks, website-link clicks, and device type where available
|
||||
- [ ] #4 Campaign analytics aggregate audit opens and CTA activity by campaign, niche, region, and timeframe
|
||||
- [ ] #5 Rybbit API failures are shown gracefully and do not break the rest of the dashboard
|
||||
- [x] #5 Rybbit API failures are shown gracefully and do not break the rest of the dashboard
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
@@ -46,4 +46,6 @@ Display anonymous analytics for generated public audit pages inside the internal
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Started implementation pass for Rybbit public-audit tracking and dashboard analytics surfaces.
|
||||
|
||||
Implemented public-audit-only Rybbit tracking, on-demand Rybbit API routes for audit/campaign activity, per-audit summary helper, dashboard Rybbit error handling, and campaign-level overall Rybbit signals. AC4 remains open for full grouping by campaign/niche/region/timeframe because Rybbit events still need a stronger audit-to-campaign join model. Verification: pnpm test 305/305; pnpm lint 0 errors.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Add MVP quality gates and operational polish
|
||||
status: In Progress
|
||||
assignee: []
|
||||
created_date: '2026-06-03 19:15'
|
||||
updated_date: '2026-06-05 19:30'
|
||||
updated_date: '2026-06-05 19:49'
|
||||
labels:
|
||||
- mvp
|
||||
- quality
|
||||
@@ -27,11 +27,11 @@ Add the final MVP quality layer: German UI consistency, i18n preparation, access
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 Core UI text is German and organized so future i18n is feasible
|
||||
- [ ] #2 No secrets are stored in source code, dashboard-editable records, logs, prompts, or raw LLM history
|
||||
- [ ] #3 Dashboard surfaces integration errors for Google, PageSpeed, OpenRouter, Playwright, SMTP, Convex jobs, and Rybbit
|
||||
- [ ] #4 Critical user flows have basic tests or repeatable verification notes: login, campaign run, audit generation, approval, send, follow-up, analytics
|
||||
- [ ] #5 Coolify deployment notes cover required environment variables, Playwright browser dependencies, exposed port, and domain assumptions
|
||||
- [x] #1 Core UI text is German and organized so future i18n is feasible
|
||||
- [x] #2 No secrets are stored in source code, dashboard-editable records, logs, prompts, or raw LLM history
|
||||
- [x] #3 Dashboard surfaces integration errors for Google, PageSpeed, OpenRouter, Playwright, SMTP, Convex jobs, and Rybbit
|
||||
- [x] #4 Critical user flows have basic tests or repeatable verification notes: login, campaign run, audit generation, approval, send, follow-up, analytics
|
||||
- [x] #5 Coolify deployment notes cover required environment variables, Playwright browser dependencies, exposed port, and domain assumptions
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
@@ -48,4 +48,6 @@ Add the final MVP quality layer: German UI consistency, i18n preparation, access
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Started implementation pass for MVP quality gates, error observability, verification notes, and deployment readiness.
|
||||
|
||||
Implemented and verified German operational readiness surfaces, secret-safe integration status rows, verification notes for critical flows, and Coolify deployment notes. Verification: pnpm test 305/305; pnpm lint 0 errors.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Add campaign performance metrics
|
||||
status: In Progress
|
||||
assignee: []
|
||||
created_date: '2026-06-03 19:15'
|
||||
updated_date: '2026-06-05 19:30'
|
||||
updated_date: '2026-06-05 19:49'
|
||||
labels:
|
||||
- mvp
|
||||
- analytics
|
||||
@@ -27,11 +27,11 @@ Build the campaign metrics layer that summarizes acquisition progress from Conve
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 Campaign dashboard shows found leads, leads with contact, Kontakt fehlt, audits created, approvals open, emails sent, follow-ups planned/sent, responses, conversations, offers, wins, and losses
|
||||
- [ ] #2 Metrics can be filtered by campaign, niche/category, PLZ/region, radius, priority, status, and timeframe
|
||||
- [ ] #3 Campaign run detail shows new leads, skipped duplicates, blacklisted/skipped leads, errors, and audits generated
|
||||
- [ ] #4 Rybbit-derived audit opens and CTA clicks are shown alongside Convex sales funnel metrics
|
||||
- [ ] #5 Metrics remain readable and lightweight, without becoming a full enterprise CRM dashboard
|
||||
- [x] #1 Campaign dashboard shows found leads, leads with contact, Kontakt fehlt, audits created, approvals open, emails sent, follow-ups planned/sent, responses, conversations, offers, wins, and losses
|
||||
- [x] #2 Metrics can be filtered by campaign, niche/category, PLZ/region, radius, priority, status, and timeframe
|
||||
- [x] #3 Campaign run detail shows new leads, skipped duplicates, blacklisted/skipped leads, errors, and audits generated
|
||||
- [x] #4 Rybbit-derived audit opens and CTA clicks are shown alongside Convex sales funnel metrics
|
||||
- [x] #5 Metrics remain readable and lightweight, without becoming a full enterprise CRM dashboard
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
@@ -48,4 +48,6 @@ Build the campaign metrics layer that summarizes acquisition progress from Conve
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Started implementation pass for campaign performance metrics and filters.
|
||||
|
||||
Implemented and verified lightweight campaign metrics query/dashboard, filter contract, run detail rows, and Rybbit-derived audit opens/CTA clicks alongside Convex metrics. Verification: pnpm test 305/305; pnpm lint 0 errors.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Trigger audit generation after PageSpeed audit
|
||||
status: In Progress
|
||||
assignee: []
|
||||
created_date: '2026-06-05 12:10'
|
||||
updated_date: '2026-06-05 19:30'
|
||||
updated_date: '2026-06-05 19:49'
|
||||
labels: []
|
||||
dependencies: []
|
||||
priority: high
|
||||
@@ -19,10 +19,10 @@ Wire the existing AI audit generation queue into the current automated flow so c
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 Successful PageSpeed audit runs queue audit generation for the lead
|
||||
- [ ] #2 Failed PageSpeed audit runs still queue audit generation when a lead was started so partial evidence can produce an audit
|
||||
- [ ] #3 Existing dedupe in queueLeadAuditGeneration prevents duplicate audit_generation runs
|
||||
- [ ] #4 Regression tests cover the PageSpeed-to-audit-generation handoff
|
||||
- [x] #1 Successful PageSpeed audit runs queue audit generation for the lead
|
||||
- [x] #2 Failed PageSpeed audit runs still queue audit generation when a lead was started so partial evidence can produce an audit
|
||||
- [x] #3 Existing dedupe in queueLeadAuditGeneration prevents duplicate audit_generation runs
|
||||
- [x] #4 Regression tests cover the PageSpeed-to-audit-generation handoff
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Notes
|
||||
@@ -31,4 +31,6 @@ Wire the existing AI audit generation queue into the current automated flow so c
|
||||
Created accidentally while implementing the PageSpeed-to-audit-generation handoff. Superseded by TASK-13 because the handoff is a prerequisite for the audit/outreach review workspace. Do not implement separately.
|
||||
|
||||
Started verification pass. Implementation notes say TASK-27 is superseded by TASK-13, so only regression coverage and existing handoff will be checked.
|
||||
|
||||
Verified existing PageSpeed-to-audit-generation handoff in pageSpeedAction. Successful and failure paths queue audit generation for the started lead, queue failures are warning-logged, existing queueLeadAuditGeneration dedupe remains in place, and regression source tests pass. Verification: pnpm test 305/305; pnpm lint 0 errors.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useQuery } from "convex/react";
|
||||
import { Activity, BarChart3, Filter, MousePointerClick } from "lucide-react";
|
||||
import { Activity, Filter, MousePointerClick } from "lucide-react";
|
||||
|
||||
import { api } from "@/convex/_generated/api";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -30,6 +30,12 @@ const metricLabels: Record<string, string> = {
|
||||
|
||||
export function AnalyticsDashboard() {
|
||||
const dashboard = useQuery(api.campaignMetrics.getDashboard, { limit: 20 });
|
||||
const [rybbitData, setRybbitData] = useState<{
|
||||
auditOpens: number;
|
||||
ctaClicks: number;
|
||||
outboundClicks: number;
|
||||
} | null>(null);
|
||||
const [rybbitError, setRybbitError] = useState<string | null>(null);
|
||||
const metricEntries = useMemo(() => {
|
||||
if (!dashboard) {
|
||||
return [];
|
||||
@@ -38,6 +44,31 @@ export function AnalyticsDashboard() {
|
||||
return Object.entries(dashboard.metrics).filter(([key]) => key in metricLabels);
|
||||
}, [dashboard]);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
fetch("/api/internal/rybbit/campaign")
|
||||
.then(async (response) => {
|
||||
const payload = await response.json();
|
||||
if (!isMounted) {
|
||||
return;
|
||||
}
|
||||
if (!payload.ok) {
|
||||
setRybbitError("Rybbit-Daten konnten nicht geladen werden.");
|
||||
}
|
||||
setRybbitData(payload.data ?? null);
|
||||
})
|
||||
.catch(() => {
|
||||
if (isMounted) {
|
||||
setRybbitError("Rybbit-Daten konnten nicht geladen werden.");
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (dashboard === undefined) {
|
||||
return (
|
||||
<section className="space-y-4">
|
||||
@@ -130,6 +161,10 @@ export function AnalyticsDashboard() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
||||
<p>Rybbit-Daten konnten nicht geladen werden, wenn API-URL, Site-ID oder API-Key fehlen.</p>
|
||||
{rybbitError ? <p className="text-destructive">{rybbitError}</p> : null}
|
||||
<p>Audit-Öffnungen: {rybbitData?.auditOpens ?? dashboard.metrics.rybbitAuditOpens}</p>
|
||||
<p>CTA-Klicks: {rybbitData?.ctaClicks ?? dashboard.metrics.rybbitCtaClicks}</p>
|
||||
<p>Website-Link-Klicks: {rybbitData?.outboundClicks ?? 0}</p>
|
||||
<p>Public-Audit Tracking läuft nur auf veröffentlichten Audit-Seiten.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -392,6 +392,7 @@ export const listFunnel = query({
|
||||
sendStatus: latestOutreach.sendStatus,
|
||||
responseStatus: latestOutreach.responseStatus,
|
||||
salesStatus: latestOutreach.salesStatus,
|
||||
doNotContactUntil: latestOutreach.doNotContactUntil ?? null,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
||||
@@ -86,6 +86,7 @@ export type LeadFunnelOutreach = {
|
||||
sendStatus?: OutreachSendStatus | null;
|
||||
responseStatus?: OutreachResponseStatus | null;
|
||||
salesStatus?: OutreachSalesStatus | null;
|
||||
doNotContactUntil?: number | null;
|
||||
};
|
||||
|
||||
export type LeadFunnelInput = {
|
||||
@@ -103,6 +104,7 @@ export type LeadFunnelInput = {
|
||||
contactPerson?: string | null;
|
||||
websiteDomain?: string | null;
|
||||
outreach?: LeadFunnelOutreach | null;
|
||||
now?: number;
|
||||
};
|
||||
|
||||
export type LeadFunnelCard = {
|
||||
@@ -303,6 +305,14 @@ function getLeadNextAction(lead: LeadFunnelInput): string {
|
||||
const stageId = getLeadFunnelStageId(lead);
|
||||
|
||||
if (stageId === "deferred") {
|
||||
if (
|
||||
lead.outreach?.salesStatus === "do_not_pursue" &&
|
||||
typeof lead.outreach.doNotContactUntil === "number" &&
|
||||
(lead.now ?? Date.now()) >= lead.outreach.doNotContactUntil
|
||||
) {
|
||||
return "Erneut prüfen";
|
||||
}
|
||||
|
||||
return "Zurückstellung prüfen";
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,12 @@ export type AuditRybbitSummary = {
|
||||
deviceTypes: string[];
|
||||
};
|
||||
|
||||
export type CampaignRybbitSummary = {
|
||||
auditOpens: number;
|
||||
ctaClicks: number;
|
||||
outboundClicks: number;
|
||||
};
|
||||
|
||||
type FetchLike = (
|
||||
input: string | URL,
|
||||
init?: RequestInit,
|
||||
@@ -117,6 +123,23 @@ export function summarizeAuditRybbitEvents(
|
||||
};
|
||||
}
|
||||
|
||||
export function summarizeCampaignRybbitEvents(
|
||||
events: RybbitEvent[],
|
||||
): CampaignRybbitSummary {
|
||||
const auditEvents = events.filter((event) => eventPath(event).startsWith("/audit/"));
|
||||
|
||||
return {
|
||||
auditOpens: auditEvents.filter((event) => event.type === "pageview").length,
|
||||
ctaClicks: auditEvents.filter((event) => {
|
||||
return event.type === "custom_event" && eventName(event) === "audit_cta_click";
|
||||
}).length,
|
||||
outboundClicks: auditEvents.filter((event) => {
|
||||
return event.type === "outbound_link" ||
|
||||
eventName(event) === "audit_website_link_click";
|
||||
}).length,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeEventsPayload(payload: unknown): RybbitEvent[] {
|
||||
if (Array.isArray(payload)) {
|
||||
return payload.filter((event): event is RybbitEvent => typeof event === "object" && event !== null);
|
||||
@@ -193,3 +216,58 @@ export async function fetchRybbitAuditAnalytics(input: {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchRybbitCampaignAnalytics(input: {
|
||||
apiUrl?: string;
|
||||
apiKey?: string;
|
||||
siteId?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
fetchImpl?: FetchLike;
|
||||
}) {
|
||||
if (!input.apiUrl || !input.apiKey || !input.siteId) {
|
||||
return {
|
||||
ok: false as const,
|
||||
error: "Rybbit ist nicht vollständig konfiguriert.",
|
||||
data: summarizeCampaignRybbitEvents([]),
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await (input.fetchImpl ?? fetch)(
|
||||
buildRybbitEventsUrl({
|
||||
apiUrl: input.apiUrl,
|
||||
siteId: input.siteId,
|
||||
startDate: input.startDate,
|
||||
endDate: input.endDate,
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${input.apiKey}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
return {
|
||||
ok: false as const,
|
||||
error: `Rybbit API Fehler ${response.status}: ${body.slice(0, 160)}`,
|
||||
data: summarizeCampaignRybbitEvents([]),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true as const,
|
||||
data: summarizeCampaignRybbitEvents(
|
||||
normalizeEventsPayload(await response.json()),
|
||||
),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false as const,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
data: summarizeCampaignRybbitEvents([]),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ test("analytics dashboard renders filters, Convex metrics, and Rybbit error stat
|
||||
assert.doesNotMatch(pageSource, /DashboardPlaceholderPage/);
|
||||
assert.match(pageSource, /AnalyticsDashboard/);
|
||||
assert.match(componentSource, /api\.campaignMetrics\.getDashboard/);
|
||||
assert.match(componentSource, /\/api\/internal\/rybbit\/campaign/);
|
||||
assert.match(componentSource, /Kampagne/);
|
||||
assert.match(componentSource, /Nische/);
|
||||
assert.match(componentSource, /PLZ/);
|
||||
@@ -69,4 +70,6 @@ test("analytics dashboard renders filters, Convex metrics, and Rybbit error stat
|
||||
assert.match(componentSource, /Status/);
|
||||
assert.match(componentSource, /Zeitraum/);
|
||||
assert.match(componentSource, /Rybbit-Daten konnten nicht geladen werden/);
|
||||
assert.match(componentSource, /Audit-Öffnungen/);
|
||||
assert.match(componentSource, /CTA-Klicks/);
|
||||
});
|
||||
|
||||
@@ -192,6 +192,27 @@ test("toLeadFunnelCard maps blocked priority to deferred stage with blocker labe
|
||||
assert.equal(card.nextAction, "Zurückstellung prüfen");
|
||||
});
|
||||
|
||||
test("toLeadFunnelCard shows do-not-contact rechecks after the block window", () => {
|
||||
const card = toLeadFunnelCard({
|
||||
id: "lead-recheck",
|
||||
companyName: "Agentur Recheck",
|
||||
priority: "medium",
|
||||
contactStatus: "do_not_contact",
|
||||
blacklistStatus: "clear",
|
||||
outreach: {
|
||||
approvalStatus: "approved",
|
||||
sendStatus: "sent",
|
||||
responseStatus: "no_interest",
|
||||
salesStatus: "do_not_pursue",
|
||||
doNotContactUntil: Date.UTC(2026, 0, 1),
|
||||
},
|
||||
now: Date.UTC(2026, 0, 2),
|
||||
});
|
||||
|
||||
assert.equal(card.stageId, "deferred");
|
||||
assert.equal(card.nextAction, "Erneut prüfen");
|
||||
});
|
||||
|
||||
test("dashboard-model exposes stable lead label helpers for UI mapping", () => {
|
||||
assert.deepEqual(leadPriorityOptions, [
|
||||
"high",
|
||||
|
||||
@@ -32,3 +32,12 @@ test("manual sales status mutation updates lead suppression states", () => {
|
||||
assert.match(outreachSource, /not_interested[\s\S]*outreachPatch\.responseStatus\s*=\s*"no_interest"/);
|
||||
assert.match(outreachSource, /do_not_pursue[\s\S]*outreachPatch\.doNotContactUntil\s*=\s*now\s*\+\s*DO_NOT_CONTACT_RECHECK_MS/);
|
||||
});
|
||||
|
||||
test("lead funnel query exposes do-not-contact recheck dates", () => {
|
||||
const leadsSource = readFileSync(
|
||||
join(process.cwd(), "convex", "leads.ts"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
assert.match(leadsSource, /doNotContactUntil:\s*latestOutreach\.doNotContactUntil/);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import test from "node:test";
|
||||
|
||||
import {
|
||||
buildRybbitEventsUrl,
|
||||
summarizeCampaignRybbitEvents,
|
||||
summarizeAuditRybbitEvents,
|
||||
type RybbitEvent,
|
||||
} from "../lib/rybbit-analytics";
|
||||
@@ -70,3 +71,19 @@ test("summarizeAuditRybbitEvents returns graceful empty metrics", () => {
|
||||
deviceTypes: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("summarizeCampaignRybbitEvents aggregates public audit activity", () => {
|
||||
assert.deepEqual(
|
||||
summarizeCampaignRybbitEvents([
|
||||
{ type: "pageview", pathname: "/audit/a" },
|
||||
{ type: "pageview", pathname: "/dashboard" },
|
||||
{ type: "custom_event", event_name: "audit_cta_click", pathname: "/audit/a" },
|
||||
{ type: "outbound_link", pathname: "/audit/a" },
|
||||
]),
|
||||
{
|
||||
auditOpens: 1,
|
||||
ctaClicks: 1,
|
||||
outboundClicks: 1,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user