Skip to content

Commit e230671

Browse files
authored
release: v0.12.0
Merge release branch v0.12.0 into main.
2 parents 92f73f5 + 4193b09 commit e230671

51 files changed

Lines changed: 15447 additions & 501 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [v0.12.0] - 2026-05-20
6+
7+
### Added
8+
- Landing hub with global site header and navigation (#124, #125)
9+
- i18n infrastructure with German (de) as first locale (#118)
10+
- i18n phase 2: `CropTranslation` table, enrichment script, locale-aware crop search
11+
- i18n phase 3: `useTranslations` wired into remaining components
12+
- `pnpm translate --fetch` / `--import` two-step plant name translation script
13+
- `dev.sh` with `up`/`down`/`build`/`rebuild` commands
14+
15+
### Fixed
16+
- GBIF enrichment: retry logic added, delay reduced to 200ms
17+
- E2E smoke tests updated for new landing hub and i18n routing
18+
519
## [v0.8.0] - 2026-05-05
620

721
### Changed

Dockerfile

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@ ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser
1818
# root@ — for direct orchestrator/manual use
1919
RUN mkdir -p /home/node/.ssh && chmod 700 /home/node/.ssh && chown node:node /home/node/.ssh && \
2020
mkdir -p /root/.ssh && chmod 700 /root/.ssh
21-
COPY deploy_keys/agent.pub /home/node/.ssh/authorized_keys
22-
RUN cp /home/node/.ssh/authorized_keys /root/.ssh/authorized_keys && \
23-
chmod 600 /home/node/.ssh/authorized_keys /root/.ssh/authorized_keys && \
21+
COPY deploy_keys/agent.pub /tmp/agent.pub
22+
COPY deploy_keys/ssh_wrapper.sh /usr/local/bin/ssh_wrapper.sh
23+
# Force node@ SSH sessions to start in /app — wrapper handles cd + original command
24+
RUN chmod +x /usr/local/bin/ssh_wrapper.sh && \
25+
printf 'command="/usr/local/bin/ssh_wrapper.sh" ' > /home/node/.ssh/authorized_keys && \
26+
cat /tmp/agent.pub >> /home/node/.ssh/authorized_keys && \
27+
cp /tmp/agent.pub /root/.ssh/authorized_keys
28+
RUN chmod 600 /home/node/.ssh/authorized_keys /root/.ssh/authorized_keys && \
2429
chown node:node /home/node/.ssh/authorized_keys && \
2530
passwd -d node && \
2631
printf '\nPort 2222\nPasswordAuthentication no\nPermitRootLogin prohibit-password\nStrictModes no\n' >> /etc/ssh/sshd_config

autodev/orchestrate/tasks.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,12 @@
2626
"role": "full-stack-engineer",
2727
"branch": "feat/67-relationship-search",
2828
"body": "The search page (`src/app/relationships/page.tsx`), API (`GET /api/relationships?q=&cursor=`), nav link from home, and 404 canonical-order fix are all already implemented on main. Do NOT reimplement them.\n\nOnly task remaining: add GET tests to `tests/api/relationships.test.ts`.\n\nAdd these test cases to the existing `describe` block (the file already has POST tests — add a new `describe('GET /api/relationships')` block):\n1. Returns 200 with array of relationships and no nextCursor when results fit in one page\n2. Returns nextCursor when results exceed limit (mock 21 rows, expect nextCursor = last id)\n3. Filters by `?q=` — only relationships where cropA or cropB name matches\n4. Accepts `?cursor=` and returns only relationships with id < cursor\n\nYou need to also mock `prisma.cropRelationship.findMany` in the vi.mock block (it is not currently mocked). Import `GET` from `@/app/api/relationships/route` at the top.\n\nRun `pnpm test:run` via SSH to verify all tests pass before committing."
29+
},
30+
{
31+
"issueNumber": 124,
32+
"title": "feat: landing hub + global site header (closes #124 #125)",
33+
"role": "frontend-engineer",
34+
"branch": "feat/124-125-header-nav-landing",
35+
"body": "Implement a global site header and replace the home page with a 3-card use-case hub. Also closes #125.\n\nDO NOT touch anything under `data/` at any point.\n\n## 1. Transparent logo\n\n`logo.png` exists at the repo root with a white background. Strip it to transparency and save as `public/logo.png`.\n\nInstall sharp: `pnpm add -D sharp`. Then run this one-off script (create, run, delete):\n\n```ts\n// scripts/strip-logo-bg.ts\nimport sharp from 'sharp'\nimport { resolve } from 'path'\nimport { mkdirSync } from 'fs'\n\nmkdirSync('public', { recursive: true })\nconst { data, info } = await sharp(resolve('logo.png')).ensureAlpha().raw().toBuffer({ resolveWithObject: true })\nconst px = new Uint8Array(data)\nfor (let i = 0; i < px.length; i += 4) {\n if (px[i] > 238 && px[i+1] > 238 && px[i+2] > 238) px[i+3] = 0\n}\nawait sharp(Buffer.from(px), { raw: { width: info.width, height: info.height, channels: 4 } }).png().toFile(resolve('public/logo.png'))\n```\n\nRun via SSH: `tsx scripts/strip-logo-bg.ts`. Delete the script after. Commit `public/logo.png`.\n\n## 2. i18n keys\n\nAdd to `messages/en.json`:\n```json\n\"Landing\": {\n \"title\": \"power2plant\",\n \"subtitle\": \"Companion planting garden planner\",\n \"lookupTitle\": \"Look up a crop\",\n \"lookupDesc\": \"Find companions and antagonists for any plant\",\n \"planTitle\": \"Plan beds\",\n \"planDesc\": \"Know what you want to grow — find what fits together\",\n \"gardenTitle\": \"My garden\",\n \"gardenDesc\": \"See how the plants in your beds get along\"\n},\n\"Nav\": {\n \"lookup\": \"Look up a crop\",\n \"plan\": \"Plan beds\",\n \"garden\": \"My garden\"\n}\n```\n\nAdd to `messages/de.json`:\n```json\n\"Landing\": {\n \"title\": \"power2plant\",\n \"subtitle\": \"Begleitpflanzung — Gartenplaner\",\n \"lookupTitle\": \"Pflanze nachschlagen\",\n \"lookupDesc\": \"Begleiter und Antagonisten für jede Pflanze finden\",\n \"planTitle\": \"Beete planen\",\n \"planDesc\": \"Du weißt, was du anbauen willst — finde, was zusammenpasst\",\n \"gardenTitle\": \"Mein Garten\",\n \"gardenDesc\": \"Schau, wie die Pflanzen in deinen Beeten miteinander auskommen\"\n},\n\"Nav\": {\n \"lookup\": \"Pflanze nachschlagen\",\n \"plan\": \"Beete planen\",\n \"garden\": \"Mein Garten\"\n}\n```\n\n## 3. SiteHeader component\n\nCreate `src/components/site-header.tsx`:\n\n```tsx\n'use client'\nimport Image from 'next/image'\nimport { Link, usePathname } from '@/i18n/navigation'\nimport { useTranslations } from 'next-intl'\nimport { LocaleSwitcher } from '@/components/locale-switcher'\nimport { AuthPanel } from '@/components/auth-panel'\n\nconst NAV_ITEMS = [\n { key: 'lookup' as const, href: '/relationships' },\n { key: 'plan' as const, href: '/plan' },\n { key: 'garden' as const, href: '/garden' },\n] satisfies { key: 'lookup' | 'plan' | 'garden'; href: string }[]\n\nfunction isActive(pathname: string, href: string): boolean {\n if (href === '/relationships') {\n return pathname.startsWith('/relationships') || pathname.startsWith('/plants')\n }\n return pathname === href || pathname.startsWith(href + '/')\n}\n\nexport function SiteHeader() {\n const t = useTranslations('Nav')\n const pathname = usePathname()\n\n return (\n <header className=\"border-b bg-background sticky top-0 z-40\">\n <div className=\"max-w-5xl mx-auto px-4 h-14 flex items-center gap-6\">\n <Link href=\"/\" className=\"flex items-center gap-2 shrink-0\">\n <Image src=\"/logo.png\" alt=\"\" width={32} height={32} />\n <span className=\"font-semibold text-sm\">power2plant</span>\n </Link>\n <nav className=\"flex items-center gap-1 flex-1 flex-wrap\">\n {NAV_ITEMS.map(({ key, href }) => (\n <Link\n key={key}\n href={href}\n className={`px-3 py-1.5 rounded text-sm transition-colors ${\n isActive(pathname, href)\n ? 'font-semibold text-foreground underline decoration-primary decoration-2 underline-offset-4'\n : 'text-muted-foreground hover:text-foreground'\n }`}\n >\n {t(key)}\n </Link>\n ))}\n </nav>\n <div className=\"flex items-center gap-3 shrink-0\">\n <LocaleSwitcher />\n <AuthPanel />\n </div>\n </div>\n </header>\n )\n}\n```\n\n## 4. Route groups\n\nNext.js route groups (folders in parentheses) don't add URL segments. Use them to apply SiteHeader only to app pages, not the landing page.\n\nCreate `src/app/[locale]/(app)/layout.tsx`:\n```tsx\nimport { SiteHeader } from '@/components/site-header'\nexport default function AppLayout({ children }: { children: React.ReactNode }) {\n return (<><SiteHeader />{children}</>)\n}\n```\n\nMove these directories into `(app)/` using `git mv`:\n- `src/app/[locale]/relationships/` → `src/app/[locale]/(app)/relationships/`\n- `src/app/[locale]/garden/` → `src/app/[locale]/(app)/garden/`\n- `src/app/[locale]/contribute/` → `src/app/[locale]/(app)/contribute/`\n- `src/app/[locale]/plants/` → `src/app/[locale]/(app)/plants/`\n- `src/app/[locale]/share/` → `src/app/[locale]/(app)/share/`\n\nAfter moving, strip the inline per-page headers from each moved page — they are now handled by SiteHeader:\n- `(app)/garden/page.tsx`: remove the header div containing `LocaleSwitcher`, `AuthPanel`, `backHome` link, and h1+subtitle. Replace with just `<h1 className=\"text-3xl font-bold\">{t('title')}</h1>` and `<p className=\"text-muted-foreground mt-1\">{t('subtitle')}</p>` in a plain `<div>`. Remove `Link`, `AuthPanel`, `LocaleSwitcher` imports.\n- `(app)/relationships/page.tsx`: remove the `backHome` Link element. Remove `Link` import from `@/i18n/navigation` if it becomes unused (check if Link is used elsewhere in the file first).\n- `(app)/contribute/page.tsx`: remove the `back` Link element (the `<Link href=\"/\">` near the top of the return). Remove `Link` import if unused.\n- `(app)/share/[token]/page.tsx`: remove the `backHome` Link element. Remove `Link` import if unused.\n- `(app)/plants/[id]/page.tsx`: check for any backHome/LocaleSwitcher/AuthPanel and remove if present.\n\n## 5. /plan page and landing hub (atomic commit)\n\nCreate `src/app/[locale]/(app)/plan/page.tsx` — copy from the current `src/app/[locale]/page.tsx` with these changes:\n- Remove the entire top `<div className=\"flex items-start justify-between gap-4\">` block (the block containing the h1 title, subtitle, contributeLink, browseLink, LocaleSwitcher, and AuthPanel)\n- Remove the `<Separator />` component and its import\n- Remove imports: `LocaleSwitcher`, `AuthPanel`, `Separator` (keep `Link` for the MyGarden button)\n- Keep all planner state and logic unchanged\n- Keep the centered MyGarden button (`<Link href=\"/garden\">...`)\n\nCreate `src/app/[locale]/(landing)/page.tsx`:\n```tsx\n'use client'\nimport Image from 'next/image'\nimport { useTranslations } from 'next-intl'\nimport { Link } from '@/i18n/navigation'\nimport { LocaleSwitcher } from '@/components/locale-switcher'\nimport { AuthPanel } from '@/components/auth-panel'\n\nconst CARDS = [\n { titleKey: 'lookupTitle', descKey: 'lookupDesc', href: '/relationships' },\n { titleKey: 'planTitle', descKey: 'planDesc', href: '/plan' },\n { titleKey: 'gardenTitle', descKey: 'gardenDesc', href: '/garden' },\n] as const\n\nexport default function LandingPage() {\n const t = useTranslations('Landing')\n return (\n <main className=\"min-h-screen px-4 py-8\">\n <div className=\"max-w-3xl mx-auto\">\n <div className=\"flex justify-end items-center gap-3 mb-8\">\n <LocaleSwitcher />\n <AuthPanel />\n </div>\n <div className=\"flex flex-col items-center text-center mb-12\">\n <Image src=\"/logo.png\" alt=\"\" width={64} height={64} className=\"mb-4\" />\n <h1 className=\"text-3xl font-bold\">{t('title')}</h1>\n <p className=\"text-muted-foreground mt-2\">{t('subtitle')}</p>\n </div>\n <div className=\"grid grid-cols-1 sm:grid-cols-3 gap-4\">\n {CARDS.map(({ titleKey, descKey, href }) => (\n <Link key={href} href={href} className=\"flex flex-col gap-2 rounded-xl border bg-card p-6 hover:shadow-md transition-shadow\">\n <h2 className=\"font-semibold\">{t(titleKey)}</h2>\n <p className=\"text-sm text-muted-foreground\">{t(descKey)}</p>\n </Link>\n ))}\n </div>\n </div>\n </main>\n )\n}\n```\n\nDelete `src/app/[locale]/page.tsx` with `git rm`. These three changes (plan page, landing page, delete old page) must be in one commit.\n\n## 6. Update e2e smoke tests\n\nIn `tests/e2e/smoke.test.ts`, replace these two existing tests:\n- `'home page loads with app heading'`\n- `'home page has contribute nav link'`\n\nWith:\n```ts\ntest('landing page shows 3 use-case cards', async ({ page }) => {\n await page.goto('/')\n await expect(page.getByRole('heading', { name: 'power2plant' })).toBeVisible()\n await expect(page.getByRole('link', { name: /look up a crop/i })).toBeVisible()\n await expect(page.getByRole('link', { name: /plan beds/i })).toBeVisible()\n await expect(page.getByRole('link', { name: /my garden/i })).toBeVisible()\n})\n\ntest('plan page has site header with nav', async ({ page }) => {\n await page.goto('/plan')\n await expect(page.getByRole('banner')).toBeVisible()\n await expect(page.getByRole('link', { name: /look up a crop/i })).toBeVisible()\n})\n```\n\n## 7. Verification\n\nRun via SSH before each commit:\n- `pnpm exec tsc --noEmit` — must pass with no errors\n- `pnpm test:run` — all unit tests must pass\n- Final: `pnpm build` — must complete without errors"
2936
}
3037
]

db/dump.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ set -e
66
# datasets (Crop / CropRelationship / their sources). Auth and per-user
77
# garden tables stay empty so we never commit personal data.
88

9+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
10+
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
11+
[ -f "$ROOT_DIR/.env" ] && . "$ROOT_DIR/.env"
12+
913
DB_URL="${DATABASE_URL:-postgresql://power2plant:power2plant@localhost:5432/power2plant}"
1014
OUT="$(dirname "$0")/seed.sql"
1115

db/restore.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
#!/bin/sh
22
set -e
33

4+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5+
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
6+
[ -f "$ROOT_DIR/.env" ] && . "$ROOT_DIR/.env"
7+
48
DB_URL="${DATABASE_URL:-postgresql://power2plant:power2plant@localhost:5432/power2plant}"
59
DUMP="$(dirname "$0")/seed.sql"
610

0 commit comments

Comments
 (0)