291 Commits

Author SHA1 Message Date
b278d275bb Polish social milestone chart curves 2026-08-17 11:19:41 +02:00
eb932ebdaa Publish milestones per channel and add Instagram
Consent is bound to the channel it was given for: approving a post on X says
nothing about Instagram. Publishing state moves from the SocialMilestone row
into SocialMilestonePost, one row per channel, where a missing row means no
consent. The dialog asks per channel, shows the text each one will publish and
keeps a separate handle for each; Instagram captions end in hashtags because a
link there is not clickable.

Also fixes three problems in the existing X path:

- A QR code already past several thresholds produced one prompt per threshold,
  and since the post quotes the current scan count, every one of them would
  have published the same number. Only the highest threshold is announced now.
- Detection ran after every unique scan and re-read the QR code's full scan
  history just to hit skipDuplicates. Known milestones are filtered first.
- A failed post stayed failed forever because the consent dialog only opens
  once. The queue now retries three times on its own, spaces first attempts by
  SOCIAL_MILESTONE_MIN_GAP_HOURS, and Settings lists every milestone per
  channel with restart and revoke.

The worker no longer renders the card itself; it downloads the image the app
renders at /s/m/<token>/og, which also serves the new square and portrait
formats. Instagram publishing stays off until SOCIAL_MILESTONE_CHANNELS and
SOCIAL_WORKER_CHANNELS both name it.

Schema changes are manual SQL, see sql/2026-08-16_*.sql. Run both before
deploying this version.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 13:46:13 +02:00
55c04761ce Polish milestone dialog and one-time delivery 2026-08-14 23:43:20 +02:00
13879e3d3a Fix milestone publisher and social previews 2026-08-14 19:35:29 +02:00
4ec70ed30f Harden milestone sharing and X publishing 2026-08-14 18:19:55 +02:00
d8f7202bf6 Fix milestone sharing previews and publisher recovery 2026-08-14 14:33:29 +02:00
e7581e488d Fix milestone sharing and test worker routing 2026-08-14 14:03:05 +02:00
8ef5221f71 Show total scans in social milestones 2026-08-14 13:08:40 +02:00
8e34f97afb Align milestone charts and self-share flow 2026-08-14 13:04:09 +02:00
aa3b4d02ab Render milestone charts from real scan history 2026-08-14 12:59:37 +02:00
925540f3c6 Improve social milestone sharing flow 2026-08-14 12:31:10 +02:00
e0c32542f9 Detect social milestones when scans arrive 2026-08-14 11:48:34 +02:00
6081b9e6ae refactor: derive email sender address dynamically from SMTP_USER 2026-08-14 13:03:12 +02:00
9d1d3a2062 SMTP_USER 2026-08-14 13:02:59 +02:00
72392e8cec info instead of timo 2026-08-14 13:02:47 +02:00
f7d82aa5bd Add consented social milestone posting 2026-08-14 09:03:21 +02:00
14c429ff30 fix 2026-08-13 10:07:57 -05:00
d2c5f2848a network 0.0.0.0 2026-08-13 08:32:11 -05:00
31cba6d883 Anpassungen 2026-08-13 06:10:01 -05:00
45f6c4d83b Add orphaned pages to sitemap and link review tool from industry pages 2026-08-13 10:04:49 +02:00
a9057b25dd Add a copy-paste command list to the staging runbook
The runbook explained every step but had no way to just work through it. Adds a checklist
and all commands in one block up front, with the prose below as the reference for what a
step does and how it fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 22:29:35 +02:00
769d06e04e Add runbook for setting up the staging environment
Standalone instructions for whoever sets up testmodul.qrmaster.net on the production
server. Written to be followed without prior context: explicit paths, an upfront list of
what must not be touched, and a stop condition in step 2 if the host URLs point at
production, which would send staging clicks into the live app.

Contains no credentials - .env.test is handed over separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 22:27:22 +02:00
d623f39c54 Give the staging database its own name
The staging database now runs as qrmaster_test instead of reusing the production name.
Container and volume already kept the two apart, but a hand-typed psql session against
two databases both called `qrmaster` looks identical on either side - the distinct name is
what makes the wrong window obvious before a DELETE lands in it.

The base compose file hardcodes `pg_isready -d qrmaster` in the db healthcheck, so the
overlay has to override the probe as well. Without it the container stays unhealthy and web
never starts, because it waits on service_healthy.

Verified against `docker compose config`: staging resolves to qrmaster_test in POSTGRES_DB,
DATABASE_URL and the healthcheck, while production still resolves to qrmaster.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 22:20:30 +02:00
40b73877b6 Ignore .env.test
The staging stack is configured through .env.test, which holds its own database password
and secrets. It was not covered by the existing .env rules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 22:14:01 +02:00
113acc073f Make the session cookie name configurable for a staging deployment
Groundwork for testmodul.qrmaster.net, a second stack running the `test` branch on a real
qrmaster.net subdomain.

Production scopes its session cookie to .qrmaster.net, so the browser sends it to every
subdomain including staging. With both environments naming the cookie `userId`, the
browser holds two cookies of the same name and cookies.get() picks one arbitrarily -
staging logins would look randomly signed-out. AUTH_COOKIE_NAME lets staging pick
`userId_test` instead. Production keeps the `userId` default; changing it there would
invalidate every existing session.

Wired getAuthCookieName() into the six places that named the cookie literally. The account
deletion route now expires both the host-only and the domain-scoped variant like the logout
route already does, instead of a single cookies().delete() that would leave the other one
behind.

NEXT_PUBLIC_WWW_URL and NEXT_PUBLIC_APP_URL become build ARGs so the same image can be
built pointing at the staging host - the defaults keep a plain production build byte
identical to before. Like COOKIE_DOMAIN these must exist at build time, because process.env
is inlined into the Edge middleware bundle.

robots.ts now serves Disallow-all unless NEXT_PUBLIC_INDEXABLE is true. Staging otherwise
returns the production robots.txt and invites crawlers to index a duplicate of www.

docker-compose.test.yml is the staging overlay. Two things it must get right, both verified
against `docker compose config`:

- db and redis need `networks: !override`. Compose MERGES the networks mapping from the base
  file, and since qrmaster-network is external and shared, a plain list left them attached
  to it - `db` would then resolve to two containers and staging could read and write the
  production database.
- The web entrypoint is replaced so `prisma migrate deploy` never runs. prisma/migrations
  stopped in April 2026 and the schema has moved on through manual SQL since, so applying
  them to a fresh database would build a stale schema. Staging gets its schema from
  `pg_dump --schema-only` against production instead.

Verified: tsc clean, production build succeeds, and the merged compose config confirms
staging keeps db/redis off the shared network while production resolves unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 22:09:56 +02:00
53ef4b3b91 Serve the app on app.qrmaster.net, marketing on www
Splits the two hostnames across one deployment. No files move: the Next app still
serves every route on both hosts, and the middleware decides per host which paths it
owns and 301s the rest. /login and /signup stay on www - all 82 marketing CTAs point
at /signup, which carries a hard canonical to www plus ad traffic.

src/lib/hosts.ts is the single source of truth for the boundary (APP_PATH_PREFIXES,
isAppPath, wwwUrl, appUrl, urlForPath). The middleware and every absolute-URL builder
read from it so they cannot drift apart.

- Split the overloaded NEXT_PUBLIC_APP_URL into a www and an app origin. It previously
  fed both public URLs and in-app URLs, so any single value was wrong somewhere. Most
  important: QRCodeCard encodes this origin into the QR code the user downloads and
  prints, so it must stay on www.
- Route Stripe return URLs, email links and OAuth redirects per path rather than
  against one origin, so /dashboard lands on app and /pricing on www.
- Cross the host boundary once, after a successful login: the router cannot push across
  origins, so that jump needs a full load. The user arrives signed in because the
  session cookie is scoped to COOKIE_DOMAIN.
- Keep the app host out of search indexes: X-Robots-Tag on every response plus a
  Disallow-all robots.txt via rewrite, and /sitemap.xml redirects to www.
- Point the TikTok callback fallback at www explicitly. It used to read
  NEXT_PUBLIC_APP_URL, whose meaning changed here, and only the apex domain is
  verified with TikTok.

Host splitting is inert while both origins are equal, so development is unaffected.

Verified: tsc clean, production build succeeds including the Edge middleware bundle,
and the path-to-host mapping is unit-checked (prefix traps like /created and
/settings-guide stay on www, query strings do not break matching).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 19:44:08 +02:00
35ea8cc3e9 Share session cookies across www and app subdomains
Groundwork for moving the app to app.qrmaster.net: the session has to survive the
host change from www.qrmaster.net to app.qrmaster.net.

- Add COOKIE_DOMAIN and apply it to the auth, CSRF, attribution and OAuth flow
  cookies. Honoured only in production, because browsers reject dotted domains on
  localhost - a prod .env copied into a dev environment would otherwise break
  every login instead of just ignoring the value.
- Expire both the host-only and the domain-scoped variant on logout. Next's
  ResponseCookies is keyed by cookie name and rewrites the entire set-cookie
  header from its internal map on every set(), so the two variants must be
  appended manually - otherwise one overwrites the other and the surviving stale
  cookie keeps the user signed in.
- Pass COOKIE_DOMAIN as both build arg and runtime env: process.env is inlined
  into the Edge middleware bundle, so a runtime-only value would leave the
  middleware and the route handlers disagreeing about the cookie scope.

No behaviour change while COOKIE_DOMAIN is unset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 19:01:14 +02:00
e3276f5943 retention 2026-08-11 23:35:57 +02:00
ca1e432f80 Clarity 2026-08-11 21:50:42 +02:00
bec48ab8e1 seo 2026-08-07 13:35:19 +02:00
68c531a1d5 umami bugfix 2026-08-06 09:33:53 -05:00
999ee79aca import script 2026-08-06 09:17:48 -05:00
87eb8c8883 bugfix 2026-08-06 09:14:01 -05:00
aac7283e59 Merge branch 'master' of git.bizmatch.net:tknuth/QR-master 2026-08-06 09:04:04 -05:00
50087f3c15 umami 2026-08-06 09:01:39 -05:00
6d29aa0be1 Mozzila extension 2026-08-06 10:58:19 +02:00
ca605f8852 SEO blog post V2 2026-08-05 22:28:34 +02:00
8c50bf71c5 SEO blog post 2026-08-05 19:32:52 +02:00
94bf162062 Extension 2026-08-03 11:38:29 +02:00
49c85288a1 Add developer ecosystem page and footer links 2026-08-02 00:11:29 +02:00
977fcdccf3 press 2026-07-30 10:00:35 +02:00
273182d32c signup code 2026-07-29 18:10:07 +02:00
8f29efaf50 seo + press V3 2026-07-29 17:40:26 +02:00
914e312a1b seo + press V2 2026-07-29 16:35:56 +02:00
e73075cdbc seo + press 2026-07-29 15:13:18 +02:00
e6fc428b15 Email marketing V3 2026-07-29 10:52:29 +02:00
6fd0ed8522 Email marketing V2 2026-07-29 10:42:21 +02:00
11fdec610f Email marketing 2026-07-29 00:02:50 +02:00
e1b6d5fcc1 email marketing 2026-07-28 13:29:35 +02:00
ab63d4b916 fix V2 2026-07-27 20:47:36 +02:00