From e7581e488dc64582b723076a3efb5e0c2ac582ca Mon Sep 17 00:00:00 2001 From: Timo Knuth Date: Fri, 14 Aug 2026 14:03:05 +0200 Subject: [PATCH] Fix milestone sharing and test worker routing --- docker-compose.test.yml | 9 ++++ scripts/social-worker/worker.py | 19 ++++--- .../dashboard/SocialMilestoneDialog.tsx | 51 +++++++++++++------ 3 files changed, 58 insertions(+), 21 deletions(-) diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 3cedde7..05b6143 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -65,6 +65,15 @@ services: - test-internal - qrmaster-network + social-worker: + container_name: qrmaster-test-social-worker + environment: + # Never resolve the ambiguous `web` alias on the shared production + # network. The test container name is unique on this Docker daemon. + QRMASTER_API_BASE: http://qrmaster-test-web:3000 + networks: !override + - test-internal + adminer: container_name: qrmaster-test-adminer ports: !reset [] diff --git a/scripts/social-worker/worker.py b/scripts/social-worker/worker.py index d01b282..e76ae1f 100644 --- a/scripts/social-worker/worker.py +++ b/scripts/social-worker/worker.py @@ -73,14 +73,15 @@ def render_card(card): start_ms = min(_timestamp(point.get("at")) for point in raw_points) end_ms = max(_timestamp(point.get("at")) for point in raw_points) span = max(1, end_ms - start_ms) - ceiling = float(trend.get("ceiling") or max(total * 1.25, 1.25)) + ceiling = 5.0 if total <= 5 else float(trend.get("ceiling") or total * 1.25) points = [(left + round((_timestamp(point.get("at")) - start_ms) / span * width), top + height - round(float(point.get("total", 0)) / ceiling * height)) for point in raw_points] target_y = top + height - round(float(trend.get("target") or total) / ceiling * height) - # Exactly five levels: for 20 scans, 5 / 10 / 15 / 20 / 25. - for index in range(1, 6): - value = total * index / 4 + # Small milestones use whole scans (1..5); larger ones keep the + # reached milestone on the fourth of five levels. + axis_values = list(range(1, 6)) if total <= 5 else [total * index / 4 for index in range(1, 6)] + for value in axis_values: y = top + height - round(value / ceiling * height) - is_target = index == 4 + is_target = value == total draw.line((left, y, left + width, y), fill="#bfdbfe" if is_target else "#dde5ef", width=2 if is_target else 1) draw.text((left - 15, y - 12), _format_axis(value), font=font("DejaVuSans.ttf", 18), fill=blue if is_target else slate, anchor="ra") draw.line(points, fill=blue, width=5, joint="curve") @@ -146,6 +147,12 @@ if __name__ == "__main__": interval = max(5, int(os.getenv("SOCIAL_WORKER_INTERVAL_SECONDS", "10"))) if os.getenv("SOCIAL_MILESTONE_POSTING_ENABLED", "").lower() not in {"true", "1", "yes"}: raise RuntimeError("Set SOCIAL_MILESTONE_POSTING_ENABLED=true to run this worker") + print(json.dumps({"worker": "social-milestones", "status": "started", "intervalSeconds": interval}), flush=True) while True: - run_once() + try: + run_once() + except Exception as error: + # Stay alive and make configuration/network errors visible in the + # container logs instead of entering a silent restart loop. + print(f"Worker cycle failed: {error}", flush=True) time.sleep(interval) diff --git a/src/components/dashboard/SocialMilestoneDialog.tsx b/src/components/dashboard/SocialMilestoneDialog.tsx index 183953c..2a7acef 100644 --- a/src/components/dashboard/SocialMilestoneDialog.tsx +++ b/src/components/dashboard/SocialMilestoneDialog.tsx @@ -12,17 +12,30 @@ type Card = { language: 'en' | 'de'; qrTitle: string; label: string; title: stri type Milestone = { id: string; qrTitle: string; threshold: number; defaultXHandle: string | null; preview: string; language: 'en' | 'de'; card: Card; brandStatus: string; brandPostUrl: string | null; brandPostError: string | null }; type BrandState = { brandStatus: string; brandPostUrl: string | null; brandPostError: string | null; selfSharedAt: string | null }; -function Trend({ trend }: { trend: NonNullable }) { +function Trend({ trend, locale }: { trend: NonNullable; locale: 'en' | 'de' }) { + const ceiling = trend.target <= 5 ? 5 : trend.target * 1.25; + const ticks = trend.target <= 5 + ? [1, 2, 3, 4, 5] + : Array.from({ length: 5 }, (_, index) => trend.target * (index + 1) / 4); const first = new Date(trend.points[0].at).getTime(); const last = Math.max(new Date(trend.points[trend.points.length - 1].at).getTime(), first + 1); const points = trend.points.map(point => { - const x = 5 + ((new Date(point.at).getTime() - first) / (last - first)) * 90; - const y = 88 - (point.total / trend.ceiling) * 72; + const x = 52 + ((new Date(point.at).getTime() - first) / (last - first)) * 356; + const y = 104 - (point.total / ceiling) * 88; return `${x},${y}`; }).join(' '); - const targetY = 88 - (trend.target / trend.ceiling) * 72; - const ticks = Array.from({ length: 5 }, (_, index) => trend.target * (index + 1) / 4); - return
{ticks.slice().reverse().map(tick => {tick.toLocaleString()})}
{ticks.map(tick => { const y = 88 - (tick / trend.ceiling) * 72; return ; })}
{trend.startLabel}{trend.endLabel}
; + const targetY = 104 - (trend.target / ceiling) * 88; + const number = new Intl.NumberFormat(locale === 'de' ? 'de-DE' : 'en-US', { maximumFractionDigits: 2 }); + return + {ticks.map(tick => { + const y = 104 - (tick / ceiling) * 88; + return {number.format(tick)}; + })} + + + {trend.startLabel} + {trend.endLabel} + ; } export function SocialMilestoneDialog() { @@ -73,16 +86,24 @@ export function SocialMilestoneDialog() { if (!milestone) return; // Open synchronously from the user gesture. Awaiting the API first can make // LinkedIn treat the new window as a blocked popup. - const shareWindow = window.open('', '_blank', 'noopener,noreferrer'); + const shareWindow = window.open('about:blank', '_blank'); + if (shareWindow) shareWindow.opener = null; setSaving('self'); try { const result = await update('self_share'); const shareUrl = `${window.location.origin}/s/m/${result.shareToken}`; const text = `${preview} ${shareUrl}`; - if (network === 'x') shareWindow?.location.replace(`https://x.com/intent/post?text=${encodeURIComponent(text)}`); + const targetUrl = network === 'x' + ? `https://x.com/intent/post?text=${encodeURIComponent(text)}` + : `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}`; + if (network === 'x') { + if (shareWindow) shareWindow.location.href = targetUrl; + else window.location.assign(targetUrl); + } else { await navigator.clipboard?.writeText(text); - shareWindow?.location.replace(`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}`); + if (shareWindow) shareWindow.location.href = targetUrl; + else window.location.assign(targetUrl); } setBrand(result.milestone); showToast(network === 'linkedin' ? 'Share text copied and LinkedIn opened.' : 'X share composer opened.', 'success'); @@ -107,13 +128,13 @@ export function SocialMilestoneDialog() { const count = card.totalUniqueScans || milestone.threshold; const status = brand?.brandStatus || milestone.brandStatus || 'pending'; return !open && setMilestone(null)}> - -
{copy.heading}{milestone.qrTitle} {copy.subtitle}
-
+ +
{copy.heading}{milestone.qrTitle} {copy.subtitle}
+
QR MASTERVerified scan milestone
-
TOTAL UNIQUE SCANS
{count.toLocaleString(milestone.language === 'de' ? 'de-DE' : 'en-US')}
{milestone.language === 'de' ? 'eindeutige Scans' : 'unique scans'}
{(card.totalScans || count).toLocaleString(milestone.language === 'de' ? 'de-DE' : 'en-US')} {milestone.language === 'de' ? 'Scans insgesamt' : 'total scans'}
- {card.trend ?
{milestone.language === 'de' ? 'Kumulierte eindeutige Scans seit Erstellung' : 'Cumulative unique scans since creation'}
:
{milestone.language === 'de' ? 'Erste Dynamik' : 'Early momentum'}
} +
UNIQUE SCANS
{count.toLocaleString(milestone.language === 'de' ? 'de-DE' : 'en-US')}
{(card.totalScans || count).toLocaleString(milestone.language === 'de' ? 'de-DE' : 'en-US')} {milestone.language === 'de' ? 'Scans insgesamt' : 'total scans'}
{card.trend ? :
{milestone.language === 'de' ? 'Noch keine Verlaufskurve verfügbar.' : 'No scan timeline available yet.'}
}
+ {card.trend &&
{milestone.language === 'de' ? 'Kumulierte eindeutige Scans' : 'Cumulative unique scans'}
}
{card.qrTitle}

{copy.consent}

{preview}
@@ -122,7 +143,7 @@ export function SocialMilestoneDialog() {
{copy.self}
{status !== 'pending' &&
{status === 'posted' ? copy.posted : status === 'failed' ? copy.failed : copy.queued}{brand?.brandPostUrl && View}
}
-
+
; }