Skip to content

Release v0.18.0#232

Draft
JustTB wants to merge 170 commits into
mainfrom
release/v0.18.0
Draft

Release v0.18.0#232
JustTB wants to merge 170 commits into
mainfrom
release/v0.18.0

Conversation

@JustTB

@JustTB JustTB commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

What's in this release

Features

  • Design system — Fraunces/DM Sans typography, new colour palette, full nav redesign with hamburger menu, centered logo, admin sub-nav
  • User account — user icon menu, account settings page, name fallback to email
  • Email verification — required on signup; SMTP env vars now passed through to app container
  • Plan wizard — multi-step flow with history, sliders, premium limits
  • Research funding — Stripe — credit top-up via Stripe Elements frontend; payment intent + webhook; credit balance + transaction ledger
  • Research funding — Mollie — EU payment provider mirroring Stripe; credit amount derived from `payment.amount.value` (security fix)
  • Research funding — Ko-fi — community donation pot via Ko-fi webhook; auto-funds top queued pair
  • Research queue — FIFO deduped execution queue (PERSONAL / POT / ADMIN triggers); distributed external research tasks with claim/review workflow
  • Research worker — background `research-worker` container; LLM executor with OpenRouter; low-balance email alert
  • Badges — INCREMENTAL (1/10/30/50/100…), PLANT (per botanical name), PAIR — awarded automatically, visible on public profile
  • Leaderboard — research count by period (all / yearly / monthly / weekly / daily), community pot entry
  • B2B billing — billing info form, VAT ID field, VAT validation, ZUGFeRD-compliant invoice generation via `invoice-service`; invoice sent after successful top-up
  • Admin UI — `/admin/research-queue` — enqueue pairs, update status, view pot balance + price; verbose SMTP test-email diagnostics
  • Search — grouped plant search with inline research voting; specific relationship type in companion badge; translated companion plant names
  • i18n — rename observation → finding across all locales
  • Enrich — restrict GBIF to supported locales + AGROVOC for Arabic; 3-stage Wikidata fallback for name translation

Infrastructure / fixes

  • `research-worker` added as always-on Docker service
  • `invoice-service` + `invoice-db` added to main compose under `profiles: [invoice]`; opt-in via `COMPOSE_PROFILES=invoice`; no impact on existing deployments
  • `scripts` container no longer depends on DB (health-check moved to entrypoint warning)
  • DB host port binding removed (no cross-stack exposure)
  • Staging deploy gated on DB Integrity workflow success
  • All SMTP, admin, cron, feedback, Stripe, and Ko-fi public env vars forwarded to app container
  • Prod deploy triggers on release published (not push to main)

DB migrations (run `pnpm prisma migrate deploy`)

Migration Change
`20260607000001_research_request_nullable_crop_b` `ResearchRequest.cropBId` nullable; replace unique index with two partial indexes
`20260608000001_add_research_funding_system` 10 new tables, 7 new enums; seeds initial ResearchPrice at €1.00
`20260610000001_add_mollie_payment_ids` `CreditTransaction.molliePaymentId`, `PotTransaction.molliePaymentId`
`20260611000001_add_user_billing_info` `user_billing_info` table
`20260611000001_distributed_research_tasks` RelationshipReason table (replaces scalar + backfills); ExternalResearchTask; ResearchModel registry; UserApiToken; `user.trustedResearcher`
`20260611000002_research_models_online` Update ResearchModel IDs to `:online` variants

ENV variables

App (docker-compose `app` service) — new in this release

Variable Required Notes
`SMTP_HOST` / `SMTP_PORT` / `SMTP_USER` / `SMTP_PASS` / `SMTP_FROM` for email now forwarded to container
`ADMIN_EMAILS` for admin comma-separated
`CRON_SECRET` for cron must match cron container
`FEEDBACK_IP_SALT` for feedback rate-limit hash salt
`STRIPE_SECRET_KEY` for Stripe `sk_live_...`
`STRIPE_WEBHOOK_SECRET` for Stripe `whsec_...`
`NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` for Stripe build arg
`MOLLIE_API_KEY` for Mollie `live_...`
`MOLLIE_REDIRECT_URL_BASE` for Mollie e.g. `https://yourapp.com\`
`NEXT_PUBLIC_KOFI_URL` for Ko-fi link build arg
`KOFI_VERIFICATION_TOKEN` for Ko-fi webhook from Ko-fi Webhooks settings page
`INVOICE_SERVICE_URL` for invoices `http://invoice-service:3000\`; leave unset to disable
`INVOICE_SERVICE_SECRET` for invoices must match invoice-service secret
`INVOICE_DEFAULT_VAT_RATE` for invoices e.g. `19`

Research worker (`research-worker` service)

Variable Required Notes
`OPENROUTER_API_KEY` yes `sk-or-...`
`LLM_BASE_URL` no default `https://openrouter.ai/api/v1\`
`LLM_MODEL` no default `perplexity/sonar-deep-research`
`RESEARCH_LOW_BALANCE_USD` no low-balance alert threshold, default `5`

Invoice service (`profiles: [invoice]`) — opt-in via `COMPOSE_PROFILES=invoice`

Variable Required Notes
`INVOICE_DB_PASSWORD` yes postgres password for invoice-db
`INVOICE_SERVICE_SECRET` yes shared with app
`SELLER_NAME` / `SELLER_ADDRESS_STREET` / `SELLER_ADDRESS_CITY` / `SELLER_ADDRESS_ZIP` yes §14 UStG required fields
`SELLER_STEUERNUMMER` OR `SELLER_VAT_ID` yes (one of) VAT ID required for EU B2B cross-border
`EMAIL_PROVIDER` / `EMAIL_FROM` yes `resend` / `postmark` / `smtp`
`RESEND_API_KEY` if using resend
`STORAGE_TYPE` / `STORAGE_PATH` no `local` (default) or `s3`

Deployment steps (beyond rebuild + deploy)

  1. Run DB migrations — `pnpm prisma migrate deploy` (6 new migrations; dry-run on staging first)
  2. Set new env vars — at minimum: `STRIPE_`, `MOLLIE_`, `KOFI_VERIFICATION_TOKEN`, `OPENROUTER_API_KEY`; confirm SMTP vars are set
  3. Register Stripe webhook — endpoint `/api/stripe/webhook`; copy `whsec_...` → `STRIPE_WEBHOOK_SECRET`
  4. Register Ko-fi webhook — endpoint `/api/kofi/webhook`; copy verification token → `KOFI_VERIFICATION_TOKEN`
  5. Configure Mollie — set redirect/webhook URLs in Mollie dashboard; set `MOLLIE_REDIRECT_URL_BASE`
  6. Enable invoice-service — add `COMPOSE_PROFILES=invoice` to `.env`; set `INVOICE_DB_PASSWORD`, `INVOICE_SERVICE_SECRET`, all `SELLER_*` and email vars; set `INVOICE_SERVICE_URL=http://invoice-service:3000\` in app env; then `docker compose up -d`
  7. research-worker auto-starts — confirm it appears in `docker ps` after deploy; no manual action needed
  8. Update research price — DB seeds €1.00 as placeholder; adjust via `/admin/research-queue` once real token costs are tracked

Test plan

  • `pnpm test run` — all tests pass
  • `pnpm tsc --noEmit` — clean
  • Migrations apply cleanly on prod DB
  • Stripe test payment → credit lands → invoice email received
  • Mollie test payment → credit lands
  • Ko-fi test webhook → pot balance increases → research item queued
  • research-worker picks up pending item and completes
  • Badges awarded after research completes
  • Admin queue page loads, price + balance correct

🤖 Generated with Claude Code


Update — v0.18.0 finalization (supersedes notes above where they conflict)

Merged after the original notes: #299, #307, #323, #292, #290, #291, #293, #294, #334, plus CI fixes.

Image build & deploy changed (#299)

  • Images build on GitHub Actionsghcr.io/ecohackerfarm/power2plant (:staging on release/*, :latest/:scripts on main). No build on the VPS.
  • VPS deploy is now docker compose pull && docker compose up -d (scripts/server/deploy.sh, staging-deploy.sh).
  • Public env vars are injected at runtime via window.__ENV__ (server reads process.env); nothing is baked into the image — it's a generic OSS artifact.

⚠️ Breaking env rename — update prod/staging .env

NEXT_PUBLIC_* dropped (runtime-injected now, not build args). The env table above uses the OLD names; use these instead:

  • NEXT_PUBLIC_APP_URLAPP_URL
  • NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYSTRIPE_PUBLISHABLE_KEY
  • NEXT_PUBLIC_KOFI_URLKOFI_URL

New env vars

  • BRAND_NAME — optional (default power2plant); runtime-configurable brand name (feat: configurable brand name at runtime (NEXT_PUBLIC_BRAND_NAME) #334).
  • GHCR_TOKEN (prod/staging) — classic PAT with read:packages to pull the private image. GHCR_USER optional (token authenticates; login defaults the username). Leave both unset only if the ghcr package is made public.

Other merges

Extra one-time deploy steps

  • Make ghcr image pullable by the VPS: keep it private + set GHCR_TOKEN, or set the package Public.
  • Rename the 3 NEXT_PUBLIC_* vars in staging/prod .env (see above).

Closes #222, #240, #241, #242, #285, #229

JustTB and others added 13 commits June 6, 2026 09:12
docker compose up --build skips profile services. Explicit build ensures
the scripts image has the latest source after each git pull.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass locale to API; API joins CropTranslation to override commonNames for both crops.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Blocks sign-in for unverified accounts so an attacker cannot
register with an ADMIN_EMAILS address and gain privileges.

- better-auth: requireEmailVerification + sendVerificationEmail via SMTP_* vars
- Dev fallback: logs verification URL to console when SMTP not configured
- auth-panel: shows "check your email" state after signup instead of auto-closing
- All 10 locales: add checkYourEmail + verifyEmailSent translation keys

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…buttons (#233)

t('COMPANION')/t('AVOID') not in Contribute namespace; use lowercase
'companion'/'avoid' keys which map to descriptive label text.

Co-authored-by: JustTB <franz.gatzke@googlemail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Users can now submit direct data entries, not just personal observations.
Updates all UI-facing strings in en/de/fr/es/pt/ru/zh-Hans/ja/hi/ar.
Keeps PERSONAL_OBSERVATION source-type label and observedDesc unchanged.

Co-authored-by: JustTB <franz.gatzke@googlemail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- ResearchRequest.cropBId now nullable; single-plant requests supported
  (partial unique indexes replace the old compound unique constraint)
- New GET /api/plants/search: returns matched plants with companions/
  antagonists grouped, plus a no-data tier with research request state
- Relationships page redesigned: one card per plant, companion/antagonist
  sections, no source count shown, URL ?q= param for back/share
- Inline vote button on no-data plant cards; POSTs single-plant research
  request to /api/research-requests
- Research requests page handles null cropB (single-plant requests)
- 31 new/updated tests; 318/318 passing

Co-authored-by: JustTB <franz.gatzke@googlemail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(search): grouped plant search with inline research voting

- ResearchRequest.cropBId now nullable; single-plant requests supported
  (partial unique indexes replace the old compound unique constraint)
- New GET /api/plants/search: returns matched plants with companions/
  antagonists grouped, plus a no-data tier with research request state
- Relationships page redesigned: one card per plant, companion/antagonist
  sections, no source count shown, URL ?q= param for back/share
- Inline vote button on no-data plant cards; POSTs single-plant research
  request to /api/research-requests
- Research requests page handles null cropB (single-plant requests)
- 31 new/updated tests; 318/318 passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): guard nullable cropB in fetch-unreviewed-pairs; refresh seed.sql

- Filter cropBId: not null in modeVoted query (single-plant requests
  must not appear as pairs in the research script)
- Use ! assertion for cropB access after the null filter
- Regenerate db/seed.sql + seed-enrichment-attempts.sql.gz to include
  the 20260607 nullable-cropB migration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(migration): use IF NOT EXISTS for partial indexes

CI loads seed.sql (which already has the indexes) then runs
prisma migrate deploy on top — causing duplicate index error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: JustTB <franz.gatzke@googlemail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(search): grouped plant search with inline research voting

- ResearchRequest.cropBId now nullable; single-plant requests supported
  (partial unique indexes replace the old compound unique constraint)
- New GET /api/plants/search: returns matched plants with companions/
  antagonists grouped, plus a no-data tier with research request state
- Relationships page redesigned: one card per plant, companion/antagonist
  sections, no source count shown, URL ?q= param for back/share
- Inline vote button on no-data plant cards; POSTs single-plant research
  request to /api/research-requests
- Research requests page handles null cropB (single-plant requests)
- 31 new/updated tests; 318/318 passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): guard nullable cropB in fetch-unreviewed-pairs; refresh seed.sql

- Filter cropBId: not null in modeVoted query (single-plant requests
  must not appear as pairs in the research script)
- Use ! assertion for cropB access after the null filter
- Regenerate db/seed.sql + seed-enrichment-attempts.sql.gz to include
  the 20260607 nullable-cropB migration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(migration): use IF NOT EXISTS for partial indexes

CI loads seed.sql (which already has the indexes) then runs
prisma migrate deploy on top — causing duplicate index error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(seed): regenerate seed.sql with migration record in _prisma_migrations

Previous dump was taken after applying the SQL directly (not via
prisma migrate deploy), so _prisma_migrations lacked the entry.
CI loads seed.sql then runs migrate deploy — seeing 1 pending migration
and failing freshness check. Now seed.sql includes the row.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: JustTB <franz.gatzke@googlemail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
localisedCrop was using a tMap built only from matched crops, so
companion plants in relationship rows got no translation. Each crop
already has its own translations field from the cropSelect — use that
directly instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace generic "Companion"/"Avoid" badge with actual type
(Companion, Attracts, Nurse crop, Trap crop, Avoid). Heading
already provides section context so the badge now adds signal
instead of duplicating it.

Adds i18n keys for ATTRACTS, NURSE, TRAP_CROP across all locales.

Co-authored-by: JustTB <franz.gatzke@googlemail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…es, and leaderboard

- Prisma: 10 new models (UserCredit, CreditTransaction, ResearchQueue, ResearchLog,
  ResearchPrice, PotTransaction, ResearchFunder, UserBadge, FeedbackVote, FeedbackComment)
  with 7 new enums; migration applied to dev DB
- Credits: atomic spend/top-up/refund via Stripe payment intents + webhook
- Community pot: Ko-fi webhook → pot ledger → auto-fund top-voted pair via cron
- Research queue: FIFO, deduped by crop pair, PERSONAL/POT/ADMIN triggers
- Badges: INCREMENTAL (1/10/30/50/100…), PLANT (per botanical name), PAIR; slug-keyed dedup
- Leaderboard API: research count by period (all/yearly/monthly/weekly/daily),
  community aggregated entry, hidden if ≤1 user in period
- Admin: /admin/research-queue page (enqueue pair, update status); nav link added
- Stripe: create-payment-intent + webhook routes (503 when keys absent)
- Ko-fi: webhook route with verification token guard, idempotent via transaction id

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JustTB and others added 6 commits June 8, 2026 14:18
Job was noisy and no longer needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(payments): add Stripe frontend payment flow

- Install @stripe/stripe-js and @stripe/react-stripe-js
- Add GET /api/credits/balance route returning user balance
- Add TopUpModal with preset €2/€5/€10 + custom amount, Stripe Elements checkout, and success state
- Add UserBalance component showing balance + top-up button in header
- Replace ResearchFundButton Ko-fi link with TopUpModal; falls back to disabled button when NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is unset

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(payments): wire UserBalance to nav, enable Stripe Tax

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: add Stripe env vars to .env.example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(payments): restore Ko-fi as fallback when Stripe not configured

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(payments): show Ko-fi and Stripe simultaneously, not as fallbacks

Ko-fi = public/anonymous funding, Stripe = private logged-in top-up.
Both render independently when their respective env vars are set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: update pnpm lockfile with @stripe/react-stripe-js and @stripe/stripe-js

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: JustTB <franz.gatzke@googlemail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(admin): add index page redirecting to /admin/feedback

* fix(admin): add landing page with nav links; add mobile hamburger menu

- /admin now shows card grid linking to sub-pages instead of redirecting
- Header admin link points to /admin (was /admin/feedback)
- Mobile: hamburger toggles slide-down nav; desktop nav unchanged

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: JustTB <franz.gatzke@googlemail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…arams types (#247)

Co-authored-by: JustTB <franz.gatzke@googlemail.com>
…243)

- credits: replace upsert+check with upsert then SELECT FOR UPDATE to prevent concurrent overdraw
- credits: add idempotency guard in applyTopUp — skip if stripePaymentIntentId already recorded
- admin queue: call refundCredits() for personal funders when admin takes over a pending entry
- pot: fix tryFundFromPot filter to check exact (cropAId, cropBId) pair via raw SQL subquery instead of broad cropA.researchQueueA.none filter that excluded all pairs sharing cropA
- badges: change PLANT badge slug to use botanicalName so two cultivars of same species share one badge
- stripe webhook: remove dead Pages Router config export
- kofi webhook: reject non-2-decimal-place currencies (JPY etc.) with 422 to prevent 100x overcrediting
- auth: refactor sendEmail helper shared by verification and password reset; add sendResetPassword
- auth-panel: add forgot password flow (email → reset link sent confirmation) and resend verification button
- reset-password page: new page at /[locale]/reset-password handling better-auth token from URL
- i18n: add 13 new Auth keys to all 10 locale files

Co-authored-by: JustTB <franz.gatzke@googlemail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…244)

* fix(enrich): restrict GBIF vernacular lookup to de/es/fr/pt

GBIF crowd-sourced coverage is near-zero for ar/hi/ja/ru/zh-Hans.
Running GBIF for those locales burns 2 API calls per crop for days
with 0 results. Add GBIF_SUPPORTED_LOCALES guard that silently
downgrades source to wikidata for unsupported locales.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(enrich): add hi to GBIF_SUPPORTED_LOCALES

GBIF returns real Hindi vernacular names (language: "hin") sourced
from Catalogue of Life — confirmed for common cultivated plants.
Coverage is partial (~20-40%) but real data exists.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(enrich): add AGROVOC source for Arabic (ar) translations

Parses the AGROVOC NT bulk dump (CC BY IGO 3.0) to build an in-memory
en→ar label index via skos:prefLabel/@ar + skos:altLabel/@en triples,
then matches each crop by botanical name, canonical name, English common
names, and synonyms. No network calls after file read — runs offline.

Usage:
  pnpm enrich:translate-names --locale ar --source agrovoc --agrovoc-dump ./agrovoc_base.nt

Download dump from https://www.fao.org/agrovoc/releases (choose NT format).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(enrich): auto-download AGROVOC dump for ar enrichment

Downloads agrovoc_core.nt.zip (~70 MB) from the FAO latestAgrovoc
redirect, extracts the NT file to a temp dir, parses it, then deletes
all temp files in a finally block — no persistent files left behind.

Pass --agrovoc-dump <path> to skip the download and use a local NT file
instead (useful for repeated test runs).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(enrich): fix AGROVOC NT parser for SKOS-XL label structure

AGROVOC uses skos-xl:prefLabel/altLabel (two-hop indirection via
xl_lang_TIMESTAMP label nodes) rather than plain skos:prefLabel literals.
Old regex matched nothing — 0 en→ar mappings.

New parser collects concept→labelNode and labelNode→text separately,
filtering to /xl_ar_/ and /xl_en_/ URIs to skip all other languages.
Joins in two passes (prefLabel then altLabel) to build en→ar index.

Tested on dev DB: 54,401 en→ar mappings built, 2,119 new ar crop
translations saved on top of 5,034 already from Wikidata.

Also moves download progress to stderr to avoid polluting stdout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(enrich): tighten AGROVOC lookup — botanical names only, Arabic script guard

Remove English commonNames from lookup candidates; only botanical name,
canonical name, and botanical synonyms are used. English common names
are too broad (e.g. 'onion' matches multiple species).

Add Arabic script guard (/[؀-ۿ]/) to reject AGROVOC results that
contain no Arabic characters — filters Latin names stored as Arabic
labels in AGROVOC.

Result: 2,371 clean ar translations saved (vs 2,119 before filter).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(enrich): rename 'synonyms' → 'botanicalSynonyms' in log labels

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(enrich): extend AGROVOC source to ru and ja

Generalize buildAgrovocIndex(ntPath, targetLang) to support any locale
with AGROVOC coverage. Add AGROVOC_SUPPORTED_LOCALES = {ar, ru, ja}.

AGROVOC has ~41K Russian and ~30K Japanese agricultural concepts —
same NT dump, same SKOS-XL parsing, different xl_{lang}_ URI filter.

Tested on dev DB:
  ru: 54,405 en→ru mappings, 20 new translations (Wikidata already covers most)
  ja: 39,691 en→ja mappings, 1,835 new translations

Also replaces the Arabic-script-only guard with isBotanicalName() which
works across all three locales (rejects Latin-name fallbacks AGROVOC
uses when no vernacular translation exists).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: JustTB <franz.gatzke@googlemail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread .github/workflows/ci-db.yml
…248)

Extends admin-digest tests with: weekly/Monday skip logic, 24h vs 7d
date window verification, and email content checks (from, to, subject,
html table rows).

Adds admin-test-email tests: auth guard, SMTP/ADMIN_EMAILS validation,
send behaviour, recipient and sender address assertions.

Also converts require('nodemailer') in test-email route to dynamic
import() — ESM is intercepted by vitest mocks, CJS require() is not.

Co-authored-by: JustTB <franz.gatzke@googlemail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
JustTB and others added 4 commits June 9, 2026 13:44
Co-authored-by: JustTB <franz.gatzke@googlemail.com>
Co-authored-by: JustTB <franz.gatzke@googlemail.com>
- auth.setup.ts: creates e2e-admin + e2e-user via sign-up API, marks
  emailVerified via Prisma, saves session cookies to .auth/admin.json
  and .auth/user.json
- playwright.config.ts: adds 'setup' project (runs auth.setup.ts first);
  chromium project depends on it
- admin.test.ts: extends existing unauth tests with:
  - non-admin user: redirect + 403 guards + no Admin header link
  - admin user: all pages reachable, card grid visible, sub-nav shown,
    all GET API endpoints return 200 with correct shapes
- ci-e2e.yml: adds ADMIN_EMAILS=e2e-admin@test.local env var
- .gitignore: excludes tests/e2e/.auth/ (session state files)

Co-authored-by: JustTB <franz.gatzke@googlemail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…cate stripe deps

Adds 20260608000001_add_research_funding_system tables/types to seed.sql.
Removes duplicate @stripe/react-stripe-js and @stripe/stripe-js entries
from package.json introduced by stash-pop merge conflict.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JustTB and others added 30 commits June 22, 2026 10:59
All NEXT_PUBLIC_* vars now flow through at runtime with no values
needed at docker build time, making the image a generic OSS artifact.

How it works:
- src/lib/client-env.ts: thin wrapper that reads window.__ENV__ in the
  browser and process.env[key] (bracket notation) on the server. Bracket
  notation bypasses Next.js DefinePlugin so the server bundle never has
  values statically inlined.
- src/app/[locale]/layout.tsx: server component injects window.__ENV__
  via an inline <script> tag rendered at request time, reading from the
  real process.env. Runs before any client JS, so window.__ENV__ is
  always set when client modules initialise.
- Client components (research-fund-button, top-up-modal, auth-client):
  replace process.env.NEXT_PUBLIC_* with clientEnv helpers.
- API routes (mcp, agent-instructions): bracket notation so server
  bundle reads from runtime env, not a build-time baked string.
- Dockerfile: remove placeholder ENV block (no longer needed).
- docker-entrypoint.sh: remove sed replacement (no longer needed),
  just migrate + start server.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The build->runtime env migration moved NEXT_PUBLIC_APP_URL into the
app service's runtime environment but missed the Stripe publishable
key and Ko-fi URL, which were build args on the prior image. With
build-time baking gone they were dropped entirely. Restore them as
runtime env so window.__ENV__ exposes them again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
With env injected at runtime via window.__ENV__, the NEXT_PUBLIC_
prefix is misleading (implies build-time baking) and a footgun: any
dot-notation process.env.NEXT_PUBLIC_X read would be inlined to "" at
build. Rename to plain names — APP_URL, STRIPE_PUBLISHABLE_KEY,
KOFI_URL — which Next.js cannot inline into the client bundle, so the
clientEnv helper is the only path to them on the client.

Updates code, compose, CI, .env.example, and deploy docs.

BREAKING (deploy): rename these three vars in staging/prod .env files.
Add brand to the runtime public-env helper (clientEnv.brand(),
default "power2plant"), injected through window.__ENV__ on the
client and read from process.env on the server. No build-time
baking — the image stays unflavored and the brand is set per
deployment through docker compose environment.

Replace user-facing brand literals: header logo, page-metadata
title, auth emails, Mollie payment/donation descriptions, feedback
digest subject, research agent instructions, low-balance admin
alert. i18n backHome strings use a {brand} placeholder.

Wire BRAND_NAME as runtime env in docker-compose.yml and
docker-compose.dev.yml; document it in .env.example.

Technical identifiers (localStorage keys, MCP server name, HTTP
User-Agent, app URL) intentionally left literal.

Stacked on #299 (feat/ghcr-build) for the runtime-env infra.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lled text

Light-mode foreground (#2D4A3E) and muted-foreground (#5A6E60) were too
similar — placeholder text was nearly indistinguishable from real input.

Apply placeholder:text-muted-foreground/50 to:
- ui/Input base component (covers all Input-based fields)
- feedback-button.tsx textareas (raw <textarea>)
- admin/feedback raw <input>
- admin/settings raw <input>

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a (landing) layout that renders SiteHeader (matching the (app)
layout), so the landing page carries the same nav/brand/auth header
as the rest of the app. Drop the landing's bespoke top bar
(LocaleSwitcher + AuthPanel) now that the header provides them.

Update the smoke tests that still assumed the old card landing with
a `power2plant` <h1>: assert the header banner + brand link and the
hero <h1> instead, and anchor the sign-in no-layout-shift check on
the banner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat: build Docker images on GH Actions, runtime env injection
fix(ui): reduce placeholder opacity to distinguish from filled text
feat: configurable brand name at runtime (NEXT_PUBLIC_BRAND_NAME)
feat(admin): research session detail + real LLM cost capture
feat(ui): public user profile page with badges
feat(ui): show genus sources on relationship detail page
feat(ui): show research funders on companion page
feat(ui): unreviewed/conflicting badges on relationship pages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
next build's PageProps validation requires dynamic route params to be
a Promise (unwrapped via React use()); the merged #292 used a sync
`{ params: { id } }` signature, which passed tsc but broke the build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
github.repository is "Ecohackerfarm/power2plant" (capitalised org),
but ghcr.io rejects uppercase repository paths, so the staging image
push failed. Hardcode the lowercase image name via an env var.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
next build runs with NODE_ENV=production but without runtime secrets
(they are injected at container start), so the module-load checks for
BETTER_AUTH_SECRET / BETTER_AUTH_URL threw while collecting page data
and broke the image build. Gate them on NEXT_PHASE so they only fire
at real runtime, not during the build phase.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The funders feature (#291) added a prisma.researchQueue.findFirst
call to the companions route, but the unit test's prisma mock lacked
researchQueue, so every test threw on undefined.findFirst. Add the
mock and default it to null (no funders).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The images publish to ghcr.io as private packages, but the VPS deploy
did `docker compose pull` with no registry auth, so it would fail
unauthorized. Add scripts/server/ghcr-login.sh (run before each pull
in deploy.sh and staging-deploy.sh) that logs the deploy user into
ghcr.io using GHCR_USER/GHCR_TOKEN from .env. No-op when GHCR_TOKEN is
unset, so making the package public later needs no code change.

Document GHCR_USER/GHCR_TOKEN (read:packages scope) in .env.example,
server-setup.md, and the setup.sh post-install hints.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fine-grained tokens don't support the GHCR container registry, so the
deploy token has to be a classic PAT with the read:packages scope.
Correct .env.example and server-setup.md accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ghcr.io authenticates by the PAT; the docker login username is a
throwaway (helper defaults it when unset). Relabel GHCR_USER as
optional and put GHCR_TOKEN first across .env.example, server-setup.md,
and setup.sh hints.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ump-prod

dump-prod-anonymized.sh pulls the private ghcr.io/.../:scripts image but
never logs in, so it only worked when the image was already cached. The
nightly staging-dump-refresh timer and any manual run hit
`error from registry: unauthorized` whenever the image is absent or the
deploy user's ghcr token has rotated.

deploy.sh authenticates via ghcr-login.sh, but that logs in the deploy
user and assumes the caller then runs compose as deploy. This script runs
its compose calls as the invoking user (root for a manual run), so log in
that same user inline before the first pull. No-op when GHCR_TOKEN is
unset. Once pulled the image is daemon-global, so later sudo -u deploy
steps in staging-dump-refresh.sh find it locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two schema-drift failures surfaced once the ghcr pull was fixed and the
dump actually ran:

- `set-admin-credentials.ts` failed with P2022 (column user.trustedResearcher
  does not exist). It ran inside dump-prod-anonymized.sh, i.e. against the
  freshly prod-restored DB, before the app applies pending migrations. The
  typed Prisma client selects every modelled column, so it needs the current
  schema. Move the step into staging-dump-refresh.sh, after the app restart
  (which runs `prisma migrate deploy`) and before the step-2 dump-save, so the
  schema is current and the known password lands in the saved staging dump.

- The anonymize step aborted on `DELETE FROM "UserApiToken"` when prod's schema
  lags staging (table not created yet). Guard it with a to_regclass check.

Also drops the now-dead `docker compose build scripts` line — the scripts
service is a pulled ghcr image with no build context ("No services to build").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(scripts): make staging dump-refresh + admin login work end-to-end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(ui): show derived genus sources on species relationship detail

1 participant