- QR MASTERVerified scan milestone
+
QR MASTERVerified scan milestone
TOTAL UNIQUE SCANS
{count.toLocaleString(milestone.language === 'de' ? 'de-DE' : 'en-US')}
{milestone.language === 'de' ? 'eindeutige Scans' : 'unique scans'}
- {card.trend ? {card.trend.recentTotal} {milestone.language === 'de' ? 'eindeutige Scans in den letzten' : 'unique scans in the last'} {card.trend.periodDays} days
: {milestone.language === 'de' ? 'Erste Dynamik' : 'Early momentum'}
}
+ {card.trend ? {milestone.language === 'de' ? 'Kumulierte eindeutige Scans seit Erstellung' : 'Cumulative unique scans since creation'}
: {milestone.language === 'de' ? 'Erste Dynamik' : 'Early momentum'}
}
{card.qrTitle}
diff --git a/src/lib/social-milestones-server.ts b/src/lib/social-milestones-server.ts
index 9ec4e70..181a26a 100644
--- a/src/lib/social-milestones-server.ts
+++ b/src/lib/social-milestones-server.ts
@@ -45,30 +45,28 @@ export async function detectSocialMilestones(qrId?: string) {
async function createCardSnapshot(qr: { id: string; title: string; createdAt: Date; user: { primaryUseCase: string | null } }) {
const now = new Date();
- const sevenDaysAgo = new Date(now);
- sevenDaysAgo.setUTCHours(0, 0, 0, 0);
- sevenDaysAgo.setUTCDate(sevenDaysAgo.getUTCDate() - 6);
const scans = await db.qRScan.findMany({
where: { qrId: qr.id, isUnique: true },
select: { ts: true },
orderBy: { ts: 'asc' },
});
- const daily = new Map
();
- for (const scan of scans) {
- if (scan.ts >= sevenDaysAgo) {
- const key = scan.ts.toISOString().slice(0, 10);
- daily.set(key, (daily.get(key) || 0) + 1);
- }
- }
- const series = Array.from({ length: 7 }, (_, index) => {
- const date = new Date(sevenDaysAgo);
- date.setUTCDate(date.getUTCDate() + index);
- return daily.get(date.toISOString().slice(0, 10)) || 0;
+ // Keep a representative, cumulative history in the immutable snapshot.
+ // It starts at QR creation and ends at the moment the milestone is detected.
+ const stride = Math.max(1, Math.ceil(scans.length / 24));
+ const points = [{ at: qr.createdAt.toISOString(), total: 0 }];
+ scans.forEach((scan, index) => {
+ const total = index + 1;
+ if (total % stride === 0 || total === scans.length) points.push({ at: scan.ts.toISOString(), total });
});
- const activeDays = series.filter(Boolean).length;
- const trend = activeDays >= 2 && scans.length >= 5
- ? { periodDays: 7, series, recentTotal: series.reduce((total, value) => total + value, 0) }
- : null;
+ const month = new Intl.DateTimeFormat('en', { month: 'short', year: '2-digit', timeZone: 'UTC' });
+ const trend = {
+ points,
+ startLabel: month.format(qr.createdAt),
+ endLabel: month.format(now),
+ target: scans.length,
+ // This makes the reached total sit one grid level below the chart top.
+ ceiling: Math.max(1.25, scans.length * 1.25),
+ };
// The snapshot is made at detection time and never silently changes after consent.
return buildMilestoneCardSnapshot({
diff --git a/src/lib/social-milestones.ts b/src/lib/social-milestones.ts
index 87a0921..e670788 100644
--- a/src/lib/social-milestones.ts
+++ b/src/lib/social-milestones.ts
@@ -48,7 +48,13 @@ export type SocialMilestoneCard = {
totalUniqueScans: number;
milestoneThreshold: number;
reachedAt: string;
- trend: { periodDays: number; series: number[]; recentTotal: number } | null;
+ trend: {
+ points: Array<{ at: string; total: number }>;
+ startLabel: string;
+ endLabel: string;
+ target: number;
+ ceiling: number;
+ } | null;
};
export function socialLocale(value?: string | null): SocialLocale {
@@ -90,7 +96,7 @@ export function buildMilestoneCardSnapshot(input: {
totalUniqueScans: number;
milestoneThreshold: number;
reachedAt: Date;
- trend: { periodDays: number; series: number[]; recentTotal: number } | null;
+ trend: SocialMilestoneCard['trend'];
locale: SocialLocale;
}): SocialMilestoneCard {
return {